Table of Contents

1. Introduction

5. Predict Proba Cosecha Enero

Know-Your-Customer

1. Introduction

Ante la perdida de clientes o deserción de clientes en una empresa de Telecomunicaciones, el precio de adquirir un nuevo cliente suele ser más alto que retener al antiguo. Ante esta problemática a partir de la construcción de un modelo análitico busco identificar segmentos de clientes que corren el riesgo de marcharse de la compañía, para así facilitar el poder involucrarlos de manera proactiva con nuevas estrategías comerciales en lugar de simplemente perderlos.

En este Notebook, comienzo con la construcción de un único tablón analítico a partir de las cosechas del mes de diciembre y enero que poseen información de clientes,productos,consumo y financiación. Con la ayuda de un análisis exploratorio de datos (EDA) pretendo encontrar patrones dentro de cada atributo que me permita obtener información relevante así como un mejor conocimiento de dominio. En base a esto pretendo modificar y crear nuevas características a partir de las características existentes para aumentar el rendimiento de mi modelo de machine learning, una vez el conjunto de datos esta completamente depurado construyo un modelo de clasificación que sea capaz de predecir la no permanencia de los clientes en la compañía todo esto con la ayuda de diferentes estrategías como la optimización de hyperparametros, proceso de regularización para evitar overfitting,ensamble de modelos entre otros.

Change Logs

2020/03/25: Fix xgboost of ensemble part. 2000 estimators broke runtime limit, so I reduced it to 800 estimators.

2. Exploratory Data Analysis (EDA)

Import Libraries and Settings

In [1]:
# Basic Libraries
import numpy as np 
import pandas as pd 
import csv
import seaborn as sns
import seaborn as sns; sns.set()

# avanced Notebook
import pandas as pd
from beakerx import *

# HTML 
import ipywidgets as widgets
from IPython.display import display, HTML
import sys

# warnings — Warning control
import warnings
warnings.filterwarnings('ignore')

# Html document analysis (web Scraping)
import requests
from bs4 import BeautifulSoup
import re

# convert to dates
import datetime
from datetime import datetime, timedelta

#Coding categorical labels in numbers
from sklearn.preprocessing import LabelEncoder

# Division dataset Train/test
from sklearn.model_selection import train_test_split

# Feature Scaling
from sklearn.preprocessing import RobustScaler

# collections
import collections
import os
#print(os.listdir("dir"))

# Visaulization
import matplotlib.pyplot as plt
import seaborn as sns
from plotnine import *

#from ggplot import *
#%matplotlib inline

# Análisis VIF
from sklearn.linear_model import LinearRegression

#Continuous variable normalization
from scipy.stats import zscore

#Metrics
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
from sklearn.metrics import accuracy_score, auc, confusion_matrix, f1_score, precision_score, recall_score, roc_curve

# Features Selection
from sklearn.feature_selection import VarianceThreshold
from sklearn.feature_selection import RFE
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
from sklearn.feature_selection import RFECV # Recursive feature elimination with cross validation 

# Classifier (machine learning algorithm) 
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.naive_bayes import GaussianNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import ExtraTreesClassifier
from xgboost import XGBClassifier

# Desbalanced Class
from imblearn.under_sampling import NearMiss
from imblearn.over_sampling import RandomOverSampler
from imblearn.combine import SMOTETomek
from imblearn.ensemble import BalancedBaggingClassifier
from sklearn.model_selection import KFold
from collections import Counter
from imblearn.metrics import classification_report_imbalanced

# save models
from sklearn.externals import joblib 

# Evaluation
from sklearn.model_selection import cross_val_score, cross_val_predict
from pylab import rcParams

# Parameter Tuning
from sklearn.model_selection import GridSearchCV

# Settings
pd.options.mode.chained_assignment = None # Stop warning when use inplace=True of fillna

Import Dataset

Plank December

In [ ]:
clientes_dic = pd.read_csv('../input/cosecha-dic-ene/clientes_dic.csv')
consumo_dic = pd.read_csv('../input/cosecha-dic-ene/consumo_dic.csv')
productos_dic = pd.read_csv('../input/cosecha-dic-ene/productos_dic.csv')
financiacion_dic = pd.read_csv('../input/cosecha-dic-ene/financiacion_dic.csv')

Plank January

In [ ]:
clientes_ene = pd.read_csv('../input/cosecha-dic-ene/clientes_ene.csv')
consumo_ene = pd.read_csv('../input/cosecha-dic-ene/consumo_ene.csv')
productos_ene = pd.read_csv('../input/cosecha-dic-ene/productos_ene.csv')
financiacion_ene= pd.read_csv('../input/cosecha-dic-ene/financiacion_ene.csv')

Plank separation

In [2]:
df_dic = pd.read_csv("df_dic.csv")
df_ene = pd.read_csv("df_ene.csv")
In [370]:
total = len(df_dic)*1.
plt.figure(figsize=(8,6))
ax = sns.countplot(x="_merge", data=df_dic, order=[0,1])
plt.title('Distribution of Target',size=25)
plt.xlabel('Permanencia y No permanencia de Clientes',va="center",size=15.1)
plt.ylabel('Frequency [%]')

for p in ax.patches:
        ax.annotate('{:.1f}%'.format(100*p.get_height()/total), (p.get_x()+0.2, p.get_height()+5), va="top", size=20)

ax.yaxis.set_ticks(np.linspace(0, total, 10))
_ = ax.set_yticklabels(map('{:.1f}%'.format, 100*ax.yaxis.get_majorticklocs()/total))

Analytical Plank Construction

Merge Diciembre

Es un ejmplo de como obtuve la target a partir de la unión de los tablones análiticos de cada mes cosecha diciembre y enero. Lo anterior son los datasets modificados.

In [1]:
df1_dic= clientes_dic.merge(consumo_dic, how='outer', indicator='union')
df2_dic = df1_dic.merge(productos_dic,how='outer', indicator='exists')
df3_dic= df2_dic.merge(financiacion_dic,how='outer', indicator='exists2')
df3_dic=df3_dic.drop(['union', 'exists'], axis=1)

Merge Enero

In [ ]:
df1_ene = clientes_ene.merge(consumo_ene, how='outer', indicator='union')
df2_ene= df1_ene.merge(productos_ene,how='outer', indicator='exists')
df3_ene = df2_ene.merge(financiacion_ene,how='outer', indicator='exists2')
df3_ene=df3_ene.drop(['union', 'exists'], axis=1)

Plank December and January

In [6]:
df_dic_ene = pd.merge(df3_dic, df3_ene, how='outer', on='id', suffixes=('_dic', '_ene'), indicator=True)
df_dic_ene =df_dic_ene .drop(['exists2_ene'], axis=1)

Merge from December Data and December and January

In [ ]:
filtro = df_dic_ene['_merge'].isin(["both","left_only"])
df_dic_ene=df_dic_ene[filtro]
df_dic_ene['_merge']= (df_dic_ene['_merge'] == 'left_only') +0

Data sampling

In [ ]:
# Splitting
#y = df_dic_ene._merge
#x = df_dic_ene.drop('_merge',axis=1)
In [ ]:
 #X_train, X_test, y_train, y_test = train_test_split(x,y,test_size=0.2,random_state=86,
                                                    #stratify = y)

December Harvest

In [ ]:
data_dic = df_dic_ene.loc[:, ~df_dic_ene.columns.str.contains("ene$")]

January Harvest

In [ ]:
data_ene = df_dic_ene.loc[:, ~df_dic_ene.columns.str.contains("dic$")]

Dataset Checking

Primero, echemos un vistazo a cómo se ve el conjunto de datos de diciembre y el conjunto de datos de enero.

In [4]:
df_dic.head()
Out[4]:
id edad facturacion antiguedad provincia num_lineas num_dt incidencia num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal conexion vel_conexion TV financiacion imp_financ descuentos _merge
0 1 63.0 216.028109 11/23/2018 08:48 AM La Rioja 5.0 NaN NaN 110.0 79.0 10897.0 12806.0 13751.0 FIBRA 50MB tv-futbol NaN NaN NaN 0
1 2 84.0 255.830842 08/22/2017 03:19 AM Vizcaya 3.0 NaN NaN 189.0 89.0 18657.0 6499.0 10862.0 FIBRA 600MB tv-futbol NaN NaN SI 0
2 3 66.0 135.768153 12/27/2001 01:50 PM Albacete 4.0 NaN NaN 129.0 30.0 15511.0 17013.0 16743.0 ADSL 35MB tv-futbol NaN NaN SI 0
3 4 69.0 255.658527 08/08/2015 10:53 AM Lugo 4.0 NaN NaN 51.0 52.0 12670.0 3393.0 6771.0 FIBRA 200MB tv-familiar NaN NaN NaN 0
4 5 25.0 22.302845 08/29/1997 02:19 AM Tarragona 2.0 2.0 NaN 183.0 3.0 23756.0 18436.0 4485.0 ADSL 10MB tv-futbol NaN NaN NaN 1
In [5]:
df_ene.head()
Out[5]:
id edad facturacion antiguedad provincia num_lineas num_dt incidencia num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal conexion vel_conexion TV financiacion imp_financ descuentos
0 1 63 216.028109 11/23/2018 08:48 AM La Rioja 5 NaN NaN 95 19 6525 7634 18520 FIBRA 50MB tv-futbol NaN NaN NaN
1 2 84 255.830842 08/22/2017 03:19 AM Vizcaya 3 NaN NaN 44 36 14471 14541 8016 FIBRA 600MB tv-futbol NaN NaN SI
2 3 66 135.768153 12/27/2001 01:50 PM Albacete 4 NaN NaN 94 27 1428 5248 7106 ADSL 35MB tv-futbol NaN NaN SI
3 4 69 255.658527 08/08/2015 10:53 AM Lugo 4 NaN NaN 186 20 20083 7372 5052 FIBRA 200MB tv-familiar NaN NaN NaN
4 6 51 99.348645 11/04/1997 11:43 AM Huelva 4 NaN NaN 37 32 19078 5009 8686 FIBRA 200MB tv-futbol NaN NaN NaN
In [7]:
df_dic.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 95467 entries, 0 to 95466
Data columns (total 20 columns):
 #   Column          Non-Null Count  Dtype  
---  ------          --------------  -----  
 0   id              95467 non-null  int64  
 1   edad            95467 non-null  float64
 2   facturacion     95467 non-null  float64
 3   antiguedad      95467 non-null  object 
 4   provincia       95467 non-null  object 
 5   num_lineas      95467 non-null  float64
 6   num_dt          6517 non-null   float64
 7   incidencia      5232 non-null   object 
 8   num_llamad_ent  95467 non-null  float64
 9   num_llamad_sal  95467 non-null  float64
 10  mb_datos        95467 non-null  float64
 11  seg_llamad_ent  95467 non-null  float64
 12  seg_llamad_sal  95467 non-null  float64
 13  conexion        95467 non-null  object 
 14  vel_conexion    95467 non-null  object 
 15  TV              95467 non-null  object 
 16  financiacion    6372 non-null   object 
 17  imp_financ      6372 non-null   float64
 18  descuentos      19154 non-null  object 
 19  _merge          95467 non-null  int64  
dtypes: float64(10), int64(2), object(8)
memory usage: 14.6+ MB

A lo largo de este Notebook realizare un proceso de limpieza y transformaciones en ambas cosechas (diciembre y enero) pero únicamente trabajaré con la Cosecha del mes de Diciembre con la cuál pretendo entrenar un modelo y predecir la probabilidad que tienen los clientes de enero en marcharse de la compañía, para así simular un escenario de la vida real donde los datos que se van a predecir vienen después.

A continuación, verifico los detalles sobre cada cosecha . Oculto el resultado de cada bloque de código para ahorrar espacio. Haga clic en la pestaña "Output" en el lado derecho de cada bloque de código para ver estos detalles.

También uso .isnull (). Sum () para verificar los datos faltantes (NaN).

In [8]:
print(df_dic.shape)
(95467, 20)
In [9]:
df_dic.describe()
Out[9]:
id edad facturacion num_lineas num_dt num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal imp_financ _merge
count 95467.000000 95467.000000 95467.000000 95467.000000 6517.000000 95467.000000 95467.000000 95467.000000 95467.000000 95467.000000 6372.000000 95467.000000
mean 49994.256832 51.462086 207.392912 3.558518 2.498082 124.815633 50.022762 12489.795898 9984.406612 10029.761342 22.268613 0.074214
std 28873.008865 19.590846 111.343491 1.086095 1.121339 72.492338 29.119904 7239.421267 5763.182070 5763.518604 10.177659 0.262120
min 1.000000 18.000000 15.000439 1.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 5.009999 0.000000
25% 24997.500000 35.000000 111.383822 3.000000 1.000000 62.000000 25.000000 6177.500000 5006.000000 5055.000000 13.432253 0.000000
50% 49970.000000 51.000000 206.808431 4.000000 3.000000 124.000000 50.000000 12466.000000 9965.000000 10024.000000 21.885534 0.000000
75% 75021.500000 68.000000 304.436599 4.000000 4.000000 188.000000 75.000000 18785.500000 14969.000000 15007.000000 31.155292 0.000000
max 100000.000000 85.000000 399.998433 5.000000 4.000000 250.000000 100.000000 25000.000000 20000.000000 20000.000000 39.990128 1.000000
In [11]:
df_dic.isnull().sum()
Out[11]:
id                    0
edad                  0
facturacion           0
antiguedad            0
provincia             0
num_lineas            0
num_dt            88950
incidencia        90235
num_llamad_ent        0
num_llamad_sal        0
mb_datos              0
seg_llamad_ent        0
seg_llamad_sal        0
conexion              0
vel_conexion          0
TV                    0
financiacion      89095
imp_financ        89095
descuentos        76313
_merge                0
dtype: int64
In [12]:
print(df_ene.shape)
(92711, 19)
In [15]:
df_ene.describe()
Out[15]:
id edad facturacion num_lineas num_dt num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal imp_financ
count 92711.000000 92711.000000 92711.000000 92711.000000 2614.000000 92711.000000 92711.000000 92711.000000 92711.000000 92711.000000 6666.000000
mean 49997.623626 51.429237 207.488700 3.560214 2.529457 125.109836 49.858960 12510.190495 9985.382781 10030.443960 22.272793
std 28863.150364 19.585913 111.239476 1.085810 1.123324 72.421075 29.208549 7217.671483 5774.903324 5786.754197 10.161969
min 1.000000 18.000000 15.000439 1.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 5.009999
25% 24963.500000 34.000000 111.368385 3.000000 2.000000 62.000000 25.000000 6232.500000 4960.000000 5010.000000 13.463536
50% 49999.000000 51.000000 207.089366 4.000000 3.000000 125.000000 50.000000 12526.000000 9998.000000 10037.000000 21.882572
75% 74990.500000 68.000000 304.349361 4.000000 4.000000 188.000000 75.000000 18742.000000 14981.000000 15036.000000 31.118611
max 100000.000000 85.000000 399.998433 5.000000 4.000000 250.000000 100.000000 25000.000000 20000.000000 20000.000000 39.991954
In [16]:
df_ene.isnull().sum()
Out[16]:
id                    0
edad                  0
facturacion           0
antiguedad            0
provincia             0
num_lineas            0
num_dt            90097
incidencia        90720
num_llamad_ent        0
num_llamad_sal        0
mb_datos              0
seg_llamad_ent        0
seg_llamad_sal        0
conexion              0
vel_conexion          0
TV                    0
financiacion      86045
imp_financ        86045
descuentos        72673
dtype: int64

En ambas cosechas se observa la presencia de datos faltantes en su mayoría superior al 80% como por ejemplo con las variables (num_dt,incidencia,financiación,impago y descuentos). Con respecto a las variables descuentos o incidencia tengo conocimiento de que los valores Missing es igual a "No"; es decir el cliente no ha tenido algún tipo de descuento o el cliente "No" ha tenido algún tipo de reclamación respectivamente.

Features Visualization

Lo primero que haré es visualizar los datos en busqueda de información valiosa/oculta dentro de cada característica. Por lo tanto en esta sesión decido visualizar los datos existentes sin imputar los datos faltantes para llevar un orden. Aunque la variable antiguedad no tiene datos fatltantes está diseñada en formato fecha por tanto todavía no la procesaré extrayendo el dato mensual por lo que no tendrá visualización.

Como dije anteriormente las variables descuentos y incidencia el valor NaN es igual "No" por tanto procederé a rellenarlas para poder visualizarlas.

Aquí utilizó métodos para crear gráficas basadas en el tipo de cada caaracterística.

In [3]:
# Continuous Data Plot
def cont_plot(df, feature_name, target_name, palettemap, hue_order, feature_scale): 
    df['Counts'] = "" # A trick to skip using an axis (either x or y) on splitting violinplot
    fig, [axis0,axis1] = plt.subplots(1,2,figsize=(10,5))
    sns.distplot(df[feature_name], ax=axis0);
    sns.violinplot(x=feature_name, y="Counts", hue=target_name, hue_order=hue_order, data=df,
                   palette=palettemap, split=True, orient='h', ax=axis1)
    axis1.set_xticks(feature_scale)
    plt.show()
    # WARNING: This will leave Counts column in dataset if you continues to use this dataset

# Categorical/Ordinal Data Plot
def cat_plot(df, feature_name, target_name, palettemap): 
    fig, [axis0,axis1] = plt.subplots(1,2,figsize=(10,5))
    df[feature_name].value_counts().plot.pie(autopct='%1.1f%%',ax=axis0)
    sns.countplot(x=feature_name, hue=target_name, data=df,
                  palette=palettemap,ax=axis1)
    plt.show()

    
survival_palette = {0: "black", 1: "orange"} # Color map for visualization

Tabla Clientes

Edad

la variable edad tiene una distribución uniforme, para una mejor visualización decido agrupar por rango de edad lo que facilita el análisis descriptivo como una generalización de patrones en los datos.

In [5]:
cont_plot(df_dic, 'edad', '_merge', survival_palette, [1, 0], range(0,85,10))
In [19]:
#ages_labels=pd.cut(x=df_dic['edad_dic'], bins=[18,19,30,45,50,60,85],
                         #labels=["18-19", "19-30", "30-45","45-50","50-60","60+"]).copy()
#cat_plot(df_dic, 'ages_labels', '_merge', survival_palette)

Facturación

In [6]:
cont_plot(df_dic, 'facturacion', '_merge', survival_palette, [1, 0], range(0,400,15))

Número de Líneas

In [7]:
cont_plot(df_dic, 'num_lineas', '_merge', survival_palette, [1, 0], range(1,6,1))

Incidencia

In [4]:
df_dic.incidencia.fillna("NO",inplace=True)
cat_plot(df_dic, 'incidencia', '_merge', survival_palette)
In [9]:
plot = pd.crosstab(index=df_dic['_merge'],
            columns=df_dic['incidencia']
                  ).apply(lambda r: r/r.sum() *100,
                          axis=0).plot(kind='bar', stacked=True)

Claramente los clientes con algún tipo de queja o reclamación están insatisfechos con la compañía y gran parte de ellos prefieren No permanecer en ella.

Número de Líneas en Impago

Como verifiqué anteriormente, num_dt tiene datos faltantes. El método de visualización no puede tratar datos faltantes, por lo que elimino las filas con datos faltantes temporalmente. Verá que no sobrescribí dataset y creé un DataFrame para su visualización (num_dt_nonan).

In [10]:
num_dt_nonan = df_dic[['num_dt','_merge']].copy().dropna(axis=0) # Copy dataframe so method won't leave Counts column in train_set
cont_plot(num_dt_nonan , 'num_dt', '_merge', survival_palette, [1, 0], range(1,5,1))
In [11]:
cat_plot(df_dic, 'num_dt', '_merge', survival_palette)

Esta variable tiene potencial para ser eliminada debido a la cantidad de datos faltantes y no parece haber diferencias que ayuden a encontrar patrones y ha clasificar la no permanencia de los clientes.

Tabla Productos

TV

In [12]:
cat_plot(df_dic, 'TV','_merge', survival_palette)

Intentando ver las diferencias entre Permanecer y No permanecer en la compañía según el tipo de paquete de tv contratado por el cliente para ver si dicha característica tiene un impacto en la No permanencia. No hay diferencias que discriminen la no permanencia en el tipo de paquete contratado independientemente del número de clientes.

Connection(type of internet connection)

In [13]:
cat_plot(df_dic, 'conexion','_merge', survival_palette)

Connection Speed

In [14]:
cat_plot(df_dic, 'vel_conexion','_merge', survival_palette)

Tabla Consumo

Número de Llamadas Entrantes

In [15]:
cont_plot(df_dic, 'num_llamad_ent', '_merge', survival_palette, [1, 0], range(0,300,63))

Número de Llamadas Salientes

In [16]:
cont_plot(df_dic, 'num_llamad_sal', '_merge', survival_palette, [1, 0], range(0,100,25))

mb de datos consumidos

In [17]:
cont_plot(df_dic, 'mb_datos', '_merge', survival_palette, [1, 0], range(0,25000,6000))

Segundos consumidos en llamadas entrantes

In [18]:
cont_plot(df_dic, 'seg_llamad_ent', '_merge', survival_palette, [1, 0], range(0,20000,5000))

Segundos consumidos en llamadas salientes

In [19]:
cont_plot(df_dic, 'seg_llamad_sal', '_merge', survival_palette, [1, 0], range(0,20000,5055))

Tabla Financiación

In [5]:
df_dic.financiacion.fillna("NO",inplace=True)
cat_plot(df_dic, 'financiacion', '_merge', survival_palette)
In [21]:
plot = pd.crosstab(index=df_dic['_merge'],
            columns=df_dic['financiacion']
                  ).apply(lambda r: r/r.sum() *100,
                          axis=0).plot(kind='bar', stacked=True)

Parece que hay diferencias que discriminen la permanencia y no permanencia en cuánto a si el cliente tiene algún préstamo para la adquisición de terminales con la compañía.

Descuentos

In [6]:
df_dic.descuentos.fillna("NO",inplace=True)
cat_plot(df_dic, 'descuentos', '_merge', survival_palette)
In [23]:
plot = pd.crosstab(index=df_dic['_merge'],
            columns=df_dic['descuentos']
                  ).apply(lambda r: r/r.sum() *100,
                          axis=0).plot(kind='bar', stacked=True)

Parece que mientras los clientes tengan algún tipo de descuento serán leales a la compañía.

Dinero Mensual Terminales Financiados

In [24]:
imp_financ_nonan = df_dic[['imp_financ','_merge']].copy().dropna(axis=0) #Copy dataframe so method won't leave Counts column in train_set
cont_plot(imp_financ_nonan, 'imp_financ', '_merge', survival_palette, [1, 0], range(5,45,8))

Althrough the graph has clear difference here, but lets zoom-in to check.

In [25]:
imp_financ_nonan = df_dic[['imp_financ','_merge']].copy()
imp_financ_nonan ['Counts'] = "" 
fig, axis = plt.subplots(1,1,figsize=(10,5))
sns.violinplot(x='imp_financ', y="Counts", hue='_merge', hue_order=[1, 0], data=imp_financ_nonan,
               palette=survival_palette, split=True, orient='h', ax=axis)
axis.set_xticks(range(5,45,8))
axis.set_xlim(-15,100)
plt.show()

Parece que los clientes que financiarón algun tipo de terminal y el pago mensual es inferior a 5€ o nada, no se sienten obligados a continuar con la compañía y deciden marcharse. Siembargo a medida que el pago mensual aumenta el compromiso de quedarse también, alcanzando puntos máximos en 10€,20€ y con una reducción abrupta a partir del punto máximo de 35€. Por tanto ante tasas muy bajas de abono o muy altas en los terminales financiados el precio suele ser un factor crítico

Decido Categorizar imp_financ en rangos de :

  1. imp_financ menor a = 5
  2. imp_financ = 5 to 10
  3. imp_financ = 10 to 15
  4. imp_financ = 15 to 20
  5. imp_financ = 20 to 35
  6. imp_financ = 35 or more

3. Feature Engineering

Al igual que en el aprendizaje automático, en lugar de proporcionar datos sin procesar al modelo, puedo mejorarlo modificando las funciones existentes y / o creando nuevas funciones. Usaré la información de la parte anterior de EDA para ayudarme a decidir cómo hacerlo.

Fill the Missing data

Tanto en la cosecha diciembre como la cosecha de enero

In [40]:
df_dic.describe()
Out[40]:
id edad facturacion num_lineas num_dt num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal imp_financ _merge
count 95467.000000 95467.000000 95467.000000 95467.000000 6517.000000 95467.000000 95467.000000 95467.000000 95467.000000 95467.000000 6372.000000 95467.000000
mean 49994.256832 51.462086 207.392912 3.558518 2.498082 124.815633 50.022762 12489.795898 9984.406612 10029.761342 22.268613 0.074214
std 28873.008865 19.590846 111.343491 1.086095 1.121339 72.492338 29.119904 7239.421267 5763.182070 5763.518604 10.177659 0.262120
min 1.000000 18.000000 15.000439 1.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 5.009999 0.000000
25% 24997.500000 35.000000 111.383822 3.000000 1.000000 62.000000 25.000000 6177.500000 5006.000000 5055.000000 13.432253 0.000000
50% 49970.000000 51.000000 206.808431 4.000000 3.000000 124.000000 50.000000 12466.000000 9965.000000 10024.000000 21.885534 0.000000
75% 75021.500000 68.000000 304.436599 4.000000 4.000000 188.000000 75.000000 18785.500000 14969.000000 15007.000000 31.155292 0.000000
max 100000.000000 85.000000 399.998433 5.000000 4.000000 250.000000 100.000000 25000.000000 20000.000000 20000.000000 39.990128 1.000000
In [41]:
df_ene.describe()
Out[41]:
id edad facturacion num_lineas num_dt num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal imp_financ
count 92711.000000 92711.000000 92711.000000 92711.000000 2614.000000 92711.000000 92711.000000 92711.000000 92711.000000 92711.000000 6666.000000
mean 49997.623626 51.429237 207.488700 3.560214 2.529457 125.109836 49.858960 12510.190495 9985.382781 10030.443960 22.272793
std 28863.150364 19.585913 111.239476 1.085810 1.123324 72.421075 29.208549 7217.671483 5774.903324 5786.754197 10.161969
min 1.000000 18.000000 15.000439 1.000000 1.000000 0.000000 0.000000 0.000000 0.000000 0.000000 5.009999
25% 24963.500000 34.000000 111.368385 3.000000 2.000000 62.000000 25.000000 6232.500000 4960.000000 5010.000000 13.463536
50% 49999.000000 51.000000 207.089366 4.000000 3.000000 125.000000 50.000000 12526.000000 9998.000000 10037.000000 21.882572
75% 74990.500000 68.000000 304.349361 4.000000 4.000000 188.000000 75.000000 18742.000000 14981.000000 15036.000000 31.118611
max 100000.000000 85.000000 399.998433 5.000000 4.000000 250.000000 100.000000 25000.000000 20000.000000 20000.000000 39.991954

Completo los datos faltantes de la cosecha de enero con fillna("NO") que son aquellas categorías donde NaN corresponde a "NO". Ha excepción de la variable num_dt y imp_financ que tanto en diciembre como en enero supera los datos faltantes con más del 80%, estimar estas variables podría introducir sesgo e invalidar o comprometer los resultados obtenidos distorcionandolos en mayor o menor grado.

gráficamente al eliminar las observaciones con datos Faltantes para poder visualizarlas, la variable num_dt no mostraba algún tipo de comportamiento en los datos que llegasen a agrupar los datos para un posterior modelo de clasificación por ende decido crear una nueva variable en su lugar llamada "info_numdt" (0: no hay líneas en impago,1 : entre 1 y 4 lineas en impago) , en cuánto a la variable imp_financ al eliminar las muestras faltantes gráficamente observe patrones interesantes, por tanto ya que los datos fuerón generados aleatoriamente el riesgo de introducir sesgo al eliminar valores Missings en esta variable es muy reducido, por ende decido tramificarla.

En cuanto a las variables categóricas de la cosecha de enero realizaré una imputación con respecto al mayor número de ocurrencias. (moda)

Los valores Faltantes en descuentos,incidencia y financiación ya fuerón completados en el mes de Diciembre. Por tanto procederé a hacer lo mismo con la cosecha de enero.

In [7]:
# Completando Missings Cosecha Enero
df_ene.descuentos.fillna("NO",inplace=True)
df_ene.incidencia.fillna("NO",inplace=True)
df_ene.financiacion.fillna("NO",inplace=True)
In [44]:
figu, axis1 = plt.subplots(1,1,figsize=(10,5))
sns.boxplot(data = df_dic, x = 'financiacion', y = df_dic['imp_financ'].dropna(), 
                  showfliers = True,palette='bright',ax=axis1)

plt.title('Distribucion de Pago Mensual en función de Terminales Financiados')
plt.xlabel('Número de Líneas')
plt.ylabel('Pago TérminalesFinanciados ')

plt.ticklabel_format(style='plain', axis='y')
df_dic.groupby('financiacion')['imp_financ'].median()
Out[44]:
financiacion
NO          NaN
SI    21.885534
Name: imp_financ, dtype: float64
In [27]:
# Proporción de Missings por variable
prop_missings_dic = df_dic.apply(lambda x:x.isnull().mean()).copy()
prop_missings_dic
Out[27]:
id                0.000000
edad              0.000000
facturacion       0.000000
antiguedad        0.000000
provincia         0.000000
num_lineas        0.000000
num_dt            0.931736
incidencia        0.000000
num_llamad_ent    0.000000
num_llamad_sal    0.000000
mb_datos          0.000000
seg_llamad_ent    0.000000
seg_llamad_sal    0.000000
conexion          0.000000
vel_conexion      0.000000
TV                0.000000
financiacion      0.000000
imp_financ        0.933254
descuentos        0.000000
_merge            0.000000
Counts            0.000000
dtype: float64
In [28]:
prop_missings_ene = df_ene.apply(lambda x:x.isnull().mean()).copy()
prop_missings_ene
Out[28]:
id                0.000000
edad              0.000000
facturacion       0.000000
antiguedad        0.000000
provincia         0.000000
num_lineas        0.000000
num_dt            0.971805
incidencia        0.000000
num_llamad_ent    0.000000
num_llamad_sal    0.000000
mb_datos          0.000000
seg_llamad_ent    0.000000
seg_llamad_sal    0.000000
conexion          0.000000
vel_conexion      0.000000
TV                0.000000
financiacion      0.000000
imp_financ        0.928099
descuentos        0.000000
dtype: float64
In [8]:
df_dic.drop(["imp_financ"], axis=1, inplace=True)
df_ene.drop(["imp_financ"], axis=1, inplace=True)

Al tener un alto % de Missings superior al 80% decido eliminar la variable imp_financ y evitar posibles problemas de colinealidad con num_dt la cual decidi categorizar en si tiene o no líneas en impago interpretando NaN como clientes sin deudas con la compañía.

Re-check for missing data.

Reshape Dataset

Obtención de Información con ayuda de Web Scraping

Debido al número de niveles que aparecen en la variable provincia (50 Niveles) decidó agrupar cada provincia con respecto a su correspondiente Comunidad Autonóma como una forma más eficiente de re categorizár dichas variables.

A partir de la información suministrada por el Instituto Nacional de Estadística https://www.ine.es/daco/daco42/codmun/cod_ccaa_provincia.htm, obtengo la información de la web como un diccionario y el proceso conciste en hallar coincidencia con cada columna (recuperada y actual) por el hecho de que algunas provincias recuperadas están escritas de forma diferente a las que tengo en mi marco de datos, una vez solucionado esto determino como clave de mi diccionario la lista de provincias actualizada a su escritura coincidente y como valor las diferentes comunidades autónomas. Con la ayuda de la función zip que funciona como iterador almaceno la información en mi diccionario después procedi a mapear la información recuperada y agrupada y así crear una nueva variable transformada llamada CCAA.

Automatizar este proceso evita el crear un diccionario manualmente y errores al introducir la información.

In [9]:
def make_soup(url: str) -> BeautifulSoup:
    res = requests.get(url)
    res.raise_for_status()
    return BeautifulSoup(res.text, 'html.parser')

def extract_purchases(soup: BeautifulSoup) -> list:
    table = soup.find('th', text=re.compile('Provincia')).find_parent('table')
    purchases = []
    for row in table.find_all('tr')[1:]:
        Cca_cell,pro_cell= row.find_all('td')[::-2]
        p = {
            'CCAA': pro_cell.text.strip(),
            'Provincia': Cca_cell.text.strip(),
            #'CPRO' : cpro_cell.text.strip(),
        }
        purchases.append(p)
    return purchases

if __name__ == '__main__':
    url = 'https://www.ine.es/daco/daco42/codmun/cod_ccaa_provincia.htm'
    soup = make_soup(url)
    purchases = extract_purchases(soup)

    from pprint import pprint
    pprint(purchases)
[{'CCAA': 'Andalucía', 'Provincia': 'Almería'},
 {'CCAA': 'Andalucía', 'Provincia': 'Cádiz'},
 {'CCAA': 'Andalucía', 'Provincia': 'Córdoba'},
 {'CCAA': 'Andalucía', 'Provincia': 'Granada'},
 {'CCAA': 'Andalucía', 'Provincia': 'Huelva'},
 {'CCAA': 'Andalucía', 'Provincia': 'Jaén'},
 {'CCAA': 'Andalucía', 'Provincia': 'Málaga'},
 {'CCAA': 'Andalucía', 'Provincia': 'Sevilla'},
 {'CCAA': 'Aragón', 'Provincia': 'Huesca'},
 {'CCAA': 'Aragón', 'Provincia': 'Teruel'},
 {'CCAA': 'Aragón', 'Provincia': 'Zaragoza'},
 {'CCAA': 'Asturias, Principado de', 'Provincia': 'Asturias'},
 {'CCAA': 'Balears, Illes', 'Provincia': 'Balears, Illes'},
 {'CCAA': 'Canarias', 'Provincia': 'Palmas, Las'},
 {'CCAA': 'Canarias', 'Provincia': 'Santa Cruz de Tenerife'},
 {'CCAA': 'Cantabria', 'Provincia': 'Cantabria'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Ávila'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Burgos'},
 {'CCAA': 'Castilla y León', 'Provincia': 'León'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Palencia'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Salamanca'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Segovia'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Soria'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Valladolid'},
 {'CCAA': 'Castilla y León', 'Provincia': 'Zamora'},
 {'CCAA': 'Castilla-La Mancha', 'Provincia': 'Albacete'},
 {'CCAA': 'Castilla-La Mancha', 'Provincia': 'Ciudad Real'},
 {'CCAA': 'Castilla-La Mancha', 'Provincia': 'Cuenca'},
 {'CCAA': 'Castilla-La Mancha', 'Provincia': 'Guadalajara'},
 {'CCAA': 'Castilla-La Mancha', 'Provincia': 'Toledo'},
 {'CCAA': 'Cataluña', 'Provincia': 'Barcelona'},
 {'CCAA': 'Cataluña', 'Provincia': 'Girona'},
 {'CCAA': 'Cataluña', 'Provincia': 'Lleida'},
 {'CCAA': 'Cataluña', 'Provincia': 'Tarragona'},
 {'CCAA': 'Comunitat Valenciana', 'Provincia': 'Alicante/Alacant'},
 {'CCAA': 'Comunitat Valenciana', 'Provincia': 'Castellón/Castelló'},
 {'CCAA': 'Comunitat Valenciana', 'Provincia': 'Valencia/València'},
 {'CCAA': 'Extremadura', 'Provincia': 'Badajoz'},
 {'CCAA': 'Extremadura', 'Provincia': 'Cáceres'},
 {'CCAA': 'Galicia', 'Provincia': 'Coruña, A'},
 {'CCAA': 'Galicia', 'Provincia': 'Lugo'},
 {'CCAA': 'Galicia', 'Provincia': 'Ourense'},
 {'CCAA': 'Galicia', 'Provincia': 'Pontevedra'},
 {'CCAA': 'Madrid, Comunidad de', 'Provincia': 'Madrid'},
 {'CCAA': 'Murcia, Región de', 'Provincia': 'Murcia'},
 {'CCAA': 'Navarra, Comunidad Foral de', 'Provincia': 'Navarra'},
 {'CCAA': 'País Vasco', 'Provincia': 'Araba/Álava'},
 {'CCAA': 'País Vasco', 'Provincia': 'Bizkaia'},
 {'CCAA': 'País Vasco', 'Provincia': 'Gipuzkoa'},
 {'CCAA': 'Rioja, La', 'Provincia': 'Rioja, La'},
 {'CCAA': 'Ciudades    Autónomas:', 'Provincia': ''},
 {'CCAA': 'Ceuta', 'Provincia': 'Ceuta'},
 {'CCAA': 'Melilla', 'Provincia': 'Melilla'}]
In [10]:
info = pd.DataFrame(purchases)

# renombrando columnas no coincidentes por escritura
info.Provincia.replace({'Balears, Illes':'Islas Baleares','Palmas, Las':'Las Palmas','Girona':'Gerona',
                   'Lleida':'Lérida','Alicante/Alacant':'Alicante', 'Castellón/Castelló':'Castellón',
                    'Valencia/València':'Valencia', 'Coruña, A':'La Coruña','Ourense':'Orense',
                    'Araba/Álava':'Álava', 'Bizkaia':'Vizcaya','Gipuzkoa':'Guipúzcoa',
                     'Rioja, La':'La Rioja'},inplace=True)
# Acortando Títulos largos para mejor ajuste en visualización
info.CCAA.replace({'Asturias, Principado de':'Asturias','Balears, Illes':'Balears',
                 'Madrid, Comunidad de':'Madrid','Murcia, Región de':'Murcia',
                  'Navarra, Comunidad Foral de':'Navarra','Rioja, La': 'Rioja'},inplace=True)

info.drop([50,51,52],axis=0,inplace=True)

# Actualizando Diccionario
clave=info['Provincia']
valor = info['CCAA']
Dict = dict(zip(clave,valor))  
In [11]:
combined_set = [df_dic,df_ene] # combined 2 datasets for more efficient processing
#impFinanc_bins = [13,21,22,25,99999]
#impFinanc_labels = ['13','21','22','25+']


# Extrae información con Split
def get_info(dataset, feature_name):
    return dataset[feature_name].map(lambda name:name.split('/')[0].split('/')[0].strip())

# Extrae información mensual
def get_month(dataset, feature_name):
    return pd.to_datetime(dataset[feature_name]).map(lambda x: x.month)
# Extrae información año columna actual
def get_year(dataset, feature_name):
    return pd.to_datetime(dataset[feature_name]).map(lambda x: x.year)
# Extrae los meses que han transcurrido desde la  fecha de alta
# hasta la fecha actual.
def diff_month(dataset, d2):
    x=datetime.now()
    dataset[d2] = pd.to_datetime(dataset[d2])
    return ((x.year - dataset[d2].dt.year) * 12 + (x.month - dataset[d2].dt.month)-1).map(lambda x: x)

# Agrupa Categórias
def cut_levels(x, threshold, new_value):
    value_counts = x.value_counts()
    labels = value_counts.index[value_counts < threshold]
    x.loc[np.in1d(x, labels)] = new_value
    return x

for dataset in combined_set:
    dataset['num_lineas']= dataset['num_lineas'].astype(object)
    dataset['InfoNumdt'] = dataset['num_dt'].notnull().astype(int)
    dataset['Month_antig'] = diff_month(dataset, 'antiguedad')
    dataset['CCAA'] = dataset['provincia'].map(Dict)

Num_Líneas

In [12]:
cat_plot(df_dic, 'num_lineas', '_merge', survival_palette)

InfoNumdt

In [13]:
cat_plot(df_dic, 'InfoNumdt', '_merge', survival_palette)
In [14]:
plot = pd.crosstab(index=df_dic['_merge'],
            columns=df_dic['InfoNumdt']
                  ).apply(lambda r: r/r.sum() *100,
                          axis=0).plot(kind='bar', stacked=True)

Parece que convertir el atributo a binaria fue la mejor opción para encontrar diferencias que agrupen a cada cliente. Por tanto los clientes de 1 a 4 líneas en impago parecen no permanecer en la compañía. Esto es interesante ya que contrarío a lo que se cree, aunque que el cliente tenga pagos pendientes no se le podrá negar el cambio de compañía. El cliente con una terminal móvil si tiene un pago a plazos, tendrá que liquidar las cuotas pendientes cuando haga la portabilidad, en la última factura.

Month_antig

In [15]:
for dataset in combined_set:
    dataset['diff_Month'] = ''
    dataset.loc[dataset['Month_antig'] < 24, 'diff_Month'] = '-2'
    dataset.loc[(dataset['Month_antig'] >= 24) & (dataset['Month_antig'] <= 84), 'diff_Month'] = '2-7'
    dataset.loc[(dataset['Month_antig'] > 84 ) & (dataset['Month_antig'] <= 120), 'diff_Month'] = '7-10'
    dataset.loc[(dataset['Month_antig'] > 120 ) & (dataset['Month_antig'] <= 240), 'diff_Month'] = '10-20'
    dataset.loc[dataset['Month_antig'] > 240, 'diff_Month'] = '20+'
In [16]:
cat_plot(df_dic, 'diff_Month', '_merge', survival_palette)

CCAA

In [17]:
fig, axis = plt.subplots(1,1,figsize=(28,5))
sns.countplot(x='CCAA', hue='_merge', data=df_dic,
              palette=survival_palette,ax=axis)
axis.set_ylim(0,10000)
plt.show()
print(df_dic['CCAA'].value_counts())
Castilla y León         17111
Andalucía               15390
Castilla-La Mancha       9399
Galicia                  7657
Cataluña                 7555
Comunitat Valenciana     5841
Aragón                   5720
País Vasco               5706
Extremadura              3845
Canarias                 3782
Navarra                  1986
Asturias                 1972
Murcia                   1967
Madrid                   1907
Balears                  1899
Rioja                    1877
Cantabria                1853
Name: CCAA, dtype: int64

Podemos ver que hay una diferencia en la supervivencia en cada nueva característica.

Dataset Cleanup and Spliting

Limpié las características no utilizadas o transformadas y dividí la columna _merge en una serie, por lo que tanto el conjunto de datos de entrenamiento como el conjunto de datos de prueba son idénticos.

In [18]:
df_dic.set_index('id', inplace=True)
df_ene.set_index('id',inplace=True)
In [20]:
df_train= df_dic.drop(['_merge','provincia','antiguedad','num_dt',
                       'Month_antig'],axis=1)
df_test = df_ene.drop(['provincia','antiguedad','num_dt','Month_antig'],axis=1)

y_train = df_dic['_merge']  # Relocate Survived target feature to y_train
In [21]:
X_train_analysis = df_train.copy()
#Codificación Etiquetas con LabelEncoder
lb_make = LabelEncoder()
#Clientes
X_train_analysis['incidencia'] = X_train_analysis['incidencia'].map({'NO': 0, 'SI': 1}).astype(int)
X_train_analysis['CCAA'] = lb_make.fit_transform(X_train_analysis['CCAA'])
X_train_analysis['diff_Month'] = lb_make.fit_transform(X_train_analysis['diff_Month'])
X_train_analysis['num_lineas'] = lb_make.fit_transform(X_train_analysis['num_lineas'])
#Productos
X_train_analysis['TV'] = X_train_analysis['TV'].map({'tv-futbol': 0, 'tv-familiar': 1, 'tv-total': 2}).astype(int)
X_train_analysis['conexion'] = X_train_analysis['conexion'].map({'ADSL': 0, 'FIBRA': 1}).astype(int)
X_train_analysis['vel_conexion'] = lb_make.fit_transform(X_train_analysis['vel_conexion'])

#Financiación
X_train_analysis['financiacion'] = X_train_analysis['financiacion'].map({'SI': 0, 'NO': 1}).astype(int)
X_train_analysis['descuentos'] = X_train_analysis['descuentos'].map({'SI': 0, 'NO': 1}).astype(int)
In [22]:
X_test_analysis = df_test.copy()
#Codificación Etiquetas con LabelEncoder
lb_make = LabelEncoder()
#Clientes
X_test_analysis['incidencia'] = X_test_analysis['incidencia'].map({'NO': 0, 'SI': 1}).astype(int)
X_test_analysis['CCAA'] = lb_make.fit_transform(X_test_analysis['CCAA'])
X_test_analysis['diff_Month'] = lb_make.fit_transform(X_test_analysis['diff_Month'])
X_test_analysis['num_lineas'] = lb_make.fit_transform(X_test_analysis['num_lineas'])
#Productos
X_test_analysis['TV'] = X_test_analysis['TV'].map({'tv-futbol': 0, 'tv-familiar': 1, 'tv-total': 2}).astype(int)
X_test_analysis['conexion'] = X_test_analysis['conexion'].map({'ADSL': 0, 'FIBRA': 1}).astype(int)
X_test_analysis['vel_conexion'] = lb_make.fit_transform(X_test_analysis['vel_conexion'])

#Financiación
X_test_analysis['financiacion'] = X_test_analysis['financiacion'].map({'SI': 0, 'NO': 1}).astype(int)
X_test_analysis['descuentos'] = X_test_analysis['descuentos'].map({'SI': 0, 'NO': 1}).astype(int)

Check for Correlations

Cuando 2 características o más tienen correlación, eso significa que se están explicando entre sí al tiempo que brindan solo una pequeña o ninguna información nueva. Las características con correlación conducirían a un sobreajuste en el modelo de aprendizaje automático, lo que podría dar como resultado una alta precisión en el conjunto de datos de entrenamiento y disminuir la precisión en el conjunto de datos de prueba.

Pero incluso si existen correlaciones, no podemos reducir descuidadamente las características. Como vi comentarios de Anton Lytyakov y GeekYoung. LongYin La lección que aprendí es no eliminar features juzgando demasiado pronto, al menos hasta que termine el análisis.

In [67]:
colormap = plt.cm.viridis
plt.figure(figsize=(14,14))
plt.title('Correlation between Features', y=1.05, size = 30)
sns.heatmap(X_train_analysis.corr(),
            linewidths=0.2, 
            vmax=2.0, 
            square=True, 
            cmap=colormap, 
            linecolor='white', 
            annot=True)
Out[67]:
<matplotlib.axes._subplots.AxesSubplot at 0x1a26c7f810>

Análisis Multicolinealidad

In [68]:
features_num_train = X_train_analysis
def calculateVIF(features_num):
    features = list(features_num.columns)
    num_features = len(features)
    
    model = LinearRegression()
    
    result = pd.DataFrame(index = ['VIF'], columns = features)
    result = result.fillna(0)
    
    for ite in range(num_features):
        x_features = features[:]
        y_featue = features[ite]
        x_features.remove(y_featue)
        
        x = features_num[x_features]
        y = features_num[y_featue]
        
        model.fit(features_num[x_features],features_num[y_featue])
        
        result[y_featue] = 1/(1 - model.score(features_num[x_features],features_num[y_featue]))
    
    return result

num_vif = features_num_train.copy(deep = True)
features = list(num_vif.columns)
num_vif = num_vif[features]

calculateVIF(num_vif)
Out[68]:
edad facturacion num_lineas incidencia num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal conexion vel_conexion TV financiacion descuentos InfoNumdt CCAA diff_Month
VIF 1.000306 1.345637 1.276994 1.000138 1.000183 1.000058 1.000134 1.000139 1.000167 1.031337 1.031255 1.061142 1.000816 1.000225 1.00025 1.000176 1.000129

No se observa Problemas de Multicolinealidad

In [69]:
X_test_analysis.head(7)
Out[69]:
edad facturacion num_lineas incidencia num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal conexion vel_conexion TV financiacion descuentos InfoNumdt CCAA diff_Month
id
1 63 216.028109 4 0 95 19 6525 7634 18520 1 9 0 1 1 0 16 0
2 84 255.830842 2 0 44 36 14471 14541 8016 1 10 0 1 0 0 15 2
3 66 135.768153 3 0 94 27 1428 5248 7106 0 8 0 1 0 0 7 1
4 69 255.658527 3 0 186 20 20083 7372 5052 1 3 1 1 1 0 11 2
6 51 99.348645 3 0 37 32 19078 5009 8686 1 3 0 1 1 0 0 3
7 55 88.062883 3 0 78 96 3032 5118 11695 0 5 0 0 1 0 8 3
8 21 73.076377 3 0 183 9 16442 7771 13478 0 7 0 1 1 0 11 1
In [70]:
X_train_analysis.head(7)
Out[70]:
edad facturacion num_lineas incidencia num_llamad_ent num_llamad_sal mb_datos seg_llamad_ent seg_llamad_sal conexion vel_conexion TV financiacion descuentos InfoNumdt CCAA diff_Month
id
1 63.0 216.028109 4 0 110.0 79.0 10897.0 12806.0 13751.0 1 9 0 1 1 0 16 0
2 84.0 255.830842 2 0 189.0 89.0 18657.0 6499.0 10862.0 1 10 0 1 0 0 15 2
3 66.0 135.768153 3 0 129.0 30.0 15511.0 17013.0 16743.0 0 8 0 1 0 0 7 1
4 69.0 255.658527 3 0 51.0 52.0 12670.0 3393.0 6771.0 1 3 1 1 1 0 11 2
5 25.0 22.302845 1 0 183.0 3.0 23756.0 18436.0 4485.0 0 1 0 1 1 1 8 3
6 51.0 99.348645 3 0 204.0 51.0 18428.0 8956.0 4764.0 1 3 0 1 1 0 0 3
7 55.0 88.062883 3 0 217.0 43.0 80.0 16406.0 19797.0 0 5 0 0 1 0 8 3

Feature Importances

Otra forma de analizar cuánto impacto podría tener una característica en la variable objetivo(No permanencia) es ver la importancia de cada características usando diferentes algoritmos en este caso lo haré con RandomForest y Árbol de decisión de manera aleatoría.

Automatización Selección de Variables

In [83]:
def automatic_selection(clas,dat_train,name):
    importances=clas.feature_importances_
    std = np.std([clas.feature_importances_ for tree in clas.estimators_],
             axis=0)
    indices = np.argsort(importances)[::-1]
    sorted_important_features=[]
    predictors=dat_train.columns
    for i in indices: 
        sorted_important_features.append(predictors[i])
    plt.figure(figsize=(12,5))
    plt.title(f"Feature Importance{name}",fontsize=20)
    plt.bar(range(np.size(predictors)), importances[indices],
       color="red", yerr=std[indices], align="center")
    plt.xticks(range(np.size(predictors)), sorted_important_features, rotation='vertical')

    plt.xlim([-1, np.size(predictors)])
    return plt

RandomForest

In [85]:
rforest_checker = RandomForestClassifier(random_state = 0)
rforest_checker.fit(X_train_analysis, y_train)
importances_df = pd.DataFrame(rforest_checker.feature_importances_, columns=['Feature_Importance'],
                              index=X_train_analysis.columns)
importances_df.sort_values(by=['Feature_Importance'], ascending=False, inplace=True)
print(importances_df)
automatic_selection(rforest_checker,X_train_analysis,"RandomForest")
                Feature_Importance
InfoNumdt                 0.362036
incidencia                0.296461
descuentos                0.102145
financiacion              0.029426
seg_llamad_sal            0.024904
mb_datos                  0.024660
seg_llamad_ent            0.024634
facturacion               0.024505
num_llamad_ent            0.023085
num_llamad_sal            0.020552
edad                      0.019301
CCAA                      0.012945
vel_conexion              0.011974
diff_Month                0.008216
num_lineas                0.007225
TV                        0.004784
conexion                  0.003145
Out[85]:
<module 'matplotlib.pyplot' from '/Users/rafaelsotosanchez/opt/anaconda3/lib/python3.7/site-packages/matplotlib/pyplot.py'>

TreeClassifier

In [84]:
rtree_checker=ExtraTreesClassifier(random_state = 0,n_jobs=1)
rtree_checker.fit(X_train_analysis, y_train)
importances_df = pd.DataFrame(rtree_checker.feature_importances_, columns=['Feature_Importance'],
                              index=X_train_analysis.columns)
importances_df.sort_values(by=['Feature_Importance'], ascending=False, inplace=True)
print(importances_df)
automatic_selection(rtree_checker,X_train_analysis,"TreeClassifier")
                Feature_Importance
InfoNumdt                 0.358804
incidencia                0.307162
descuentos                0.102446
financiacion              0.026818
num_llamad_ent            0.019544
facturacion               0.019183
seg_llamad_sal            0.019041
mb_datos                  0.019033
num_llamad_sal            0.019000
seg_llamad_ent            0.018974
edad                      0.018840
CCAA                      0.017153
vel_conexion              0.015114
num_lineas                0.012622
diff_Month                0.012505
TV                        0.008332
conexion                  0.005430
Out[84]:
<module 'matplotlib.pyplot' from '/Users/rafaelsotosanchez/opt/anaconda3/lib/python3.7/site-packages/matplotlib/pyplot.py'>

Aunque el resultado aquí es bastante aleatorio parece que ambos algoritmos coinciden en que las variables InfoNumdt,incidencia, descuentos, como las más importantes.

Feature Selection

En esta parte, seleccionaré características basandome en diferentes métodos que consisten en eliminación de características con varianza constante, selección de características univariadas, eliminación recursivas de características (RFE), eliminación recursivas de características con validación cruzada (RFECV) y selección de características basadas en árboles. Construire un modelo de clasificación para predecir utilizando como algoritmo RandomForest.

Funciones Saca Métricas

In [43]:
## métricas
def saca_metricas(y1, y2):
    false_positive_rate, recall, thresholds = roc_curve(y1, y2)
    roc_auc = auc(false_positive_rate, recall)
    print('AUC')
    print(roc_auc)
    plt.plot(false_positive_rate, recall, 'b')
    plt.plot([0, 1], [0, 1], 'r--')
    plt.title('AUC = %0.2f' % roc_auc)
    
    
 #funcion para mostrar los resultados
def mostrar_resultados(y_test, pred_y):
    conf_matrix = confusion_matrix(y_test, pred_y)
    plt.figure(figsize=(8,8))
    sns.heatmap(conf_matrix, xticklabels=True, yticklabels=True, annot=True, fmt="d");
    plt.title("Confusion matrix")
    plt.ylabel('True class')
    plt.xlabel('Predicted class')
    plt.show()
    print (classification_report(y_test, pred_y))
    
def draw_confusion_matrices(confusion_matricies,class_names):
    class_names = class_names.tolist()
    for cm in confusion_matrices:
        classifier, cm = cm[0], cm[1]
        sns.heatmap(cm, xticklabels=True, yticklabels=True, annot=True,fmt="d");
        plt.title('Confusion matrix for %s' % classifier)
        plt.xlabel('Predicted')
        plt.ylabel('True')
        plt.show()    

train_test_split Cosecha Diciembre

In [25]:
# ya lo había hecho anteriormente pero soló para no olvidar lo comento aquí
target=df_dic['_merge']
target.unique()
Out[25]:
array([0, 1])
In [26]:
target.head()
Out[26]:
id
1    0
2    0
3    0
4    0
5    1
Name: _merge, dtype: int64
In [27]:
X_train, X_test, y_train, y_test = train_test_split(X_train_analysis,
                        target,
                        test_size=0.2,
                        random_state=56,
                        stratify = target)
In [28]:
print(X_train.shape,X_test.shape)
(76373, 17) (19094, 17)

Análisis de Características Constantes

Con el fin de encontrar aquellas características que no proporcionan información a mi modelo y lo limiten encuanto a discriminar o predecir la variable objetivo. Para identificar características constantes, usaré la función VarianceThreshold de sklearn.

Variance Threshold

Su función es eliminar todas las features cuya variación no alcanza algún umbral. Por defecto, elimina todas las features de variación cero, es decir, las características que tienen el mismo valor en todas las muestras.

In [91]:
varModel=VarianceThreshold(threshold=0) #Estableciendo umbral de variación a 0
varModel.fit(X_train)
constArr=varModel.get_support()
#get_support() retorna True y False value para cada feature.
#True: Not a constant feature
#False: Constant feature
In [92]:
# Contando el número de features constantes y no constantes
collections.Counter(constArr)
#Non Constant feature:17
# Por tanto No hay features constantes
Out[92]:
Counter({True: 17})

Selección de características univariadas y RandomForest

Este método consiste en tomar como parámetro una función de puntuación , en este caso calculará la estadística chi2 entre cada característica. Un valor pequeño índicara que la variable es independiente de "y" , por otro lado un valor grande significará que la variable no está relacionada aleatoriamente con "y". En este caso el parámetro k conserva las características cuyos valores sean distintos de "y", el número de características almacenadas deberá proporcionarse en este caso eligiré 5.

Documentación : http://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.SelectKBest.html#sklearn.feature_selection.SelectKBes

In [93]:
# find best scored 5 features
select_feature = SelectKBest(chi2, k=5).fit(X_train, y_train)
print('Score list:', select_feature.scores_)
print('Feature list:', X_train.columns)
Score list: [5.49364719e+00 3.98635458e+01 2.57966420e-01 1.80154325e+04
 6.81110988e+01 1.54116510e+01 4.80368092e-01 1.12457767e+03
 4.34842504e+02 2.79726662e-01 7.65101820e+00 3.80105081e-01
 2.80198654e+01 3.08129681e+02 2.25458182e+04 8.60024312e+00
 4.35443715e-01]
Feature list: Index(['edad', 'facturacion', 'num_lineas', 'incidencia', 'num_llamad_ent',
       'num_llamad_sal', 'mb_datos', 'seg_llamad_ent', 'seg_llamad_sal',
       'conexion', 'vel_conexion', 'TV', 'financiacion', 'descuentos',
       'InfoNumdt', 'CCAA', 'diff_Month'],
      dtype='object')
In [94]:
atrib = select_feature.get_support()
atributos = [X_train.columns[i] for i in list(atrib.nonzero()[0])]
atributos
Out[94]:
['incidencia', 'seg_llamad_ent', 'seg_llamad_sal', 'descuentos', 'InfoNumdt']

Este algoritmo selecciona a los mejores atributos basándose en una prueba estadística univariante. Al objeto SelectKBest le pasamos la prueba estadística chi2, en este caso una junto con el número de atributos a seleccionar k=5. El algoritmo va a aplicar la prueba a todos los atributos y va a seleccionar los que mejor resultado obtuvieron. Como podemos ver, el algoritmo seleccionó la cantidad de atributos que le indique; en este ejemplo decidí seleccionar solo 5.

Este método me selecciona como las 5 mejores características para clasificar: InfoNumdt, Incidencia,seg_llamad_ent,seg_llamad_sal,descuentos

Entrenando Modelo 1

Aquí entrenare un primer modelo con las 5 mejores caraterísticas seleccionadas por el método Univariado.

In [144]:
X_train_2 = select_feature.transform(X_train)
X_test_2 = select_feature.transform(X_test)

#LogisticRegression classifier with n_estimators=10 (default)
classifier = RandomForestClassifier()      
classifier = classifier.fit(X_train_2,y_train)
y_pred_1 = classifier.predict(X_test_2)
mostrar_resultados(y_test, y_pred_1)
saca_metricas(y_test, y_pred_1)

score_1 = classifier.score(X_train_2,y_train)
#ac_2 = accuracy_score(y_test,classifier.predict(X_test_2))
#print('Accuracy is: ',ac_2)
#cm_2 = confusion_matrix(y_test,classifier.predict(X_test_2))
#sns.heatmap(cm_2,annot=True,fmt="d")
              precision    recall  f1-score   support

           0       0.99      0.98      0.99     17677
           1       0.78      0.92      0.84      1417

    accuracy                           0.97     19094
   macro avg       0.88      0.95      0.91     19094
weighted avg       0.98      0.97      0.98     19094

AUC
0.9477274493859046

Eliminación recursiva de características (RFE) con RandomForest

http://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.RFE.html Básicamente, utiliza uno de los métodos de clasificación (RandomForest en nuestro ejemplo), y asigna pesos a cada una de las características. Los pesos absolutos más pequeños se eliminan de las características del conjunto actual. Ese procedimiento se repite de forma recursiva en el conjunto podado hasta el número deseado de características.

In [98]:
# Create the RFE object and rank each pixel
  
classifier_2 = RandomForestClassifier()      
rfe = RFE(estimator=classifier_2, n_features_to_select=5, step=1)
rfe = rfe.fit(X_train, y_train)
In [99]:
print('Chosen best 5 feature by rfe:',X_train.columns[rfe.support_])
Chosen best 5 feature by rfe: Index(['facturacion', 'incidencia', 'mb_datos', 'descuentos', 'InfoNumdt'], dtype='object')

Las 5 mejores características elegidas por rfe son incidencia,mb_datos,seg_llamad_sal, descuentos,nfoNumdt. Como no son exactamente similares con el método anterior (selectkBest) al diferir en 1 característica. Por lo tanto, calcularé la precisión nuevamente. En breve, podre determinar si realicé una buena selección de funciones con los métodos rfe y selectkBest. Sin embargo, encontré las 5 mejores característica con dos métodos diferentes. pero no necesariamente deben ser 5 características, quizás alomejor con tan sólo 2 características o 15, se obtendría la misma precisión o mejor precisión, por lo tanto veamos cuántas características necesito usar con el método rfecv.

Entrenando Modelo 2

In [143]:
X_train_3 = rfe.transform(X_train)
X_test_3 = rfe.transform(X_test)

#LogisticRegression classifier with n_estimators=10 (default)
classifier_2= RandomForestClassifier()    
classifier_2= classifier_2.fit(X_train_3,y_train)
y_pred = classifier_2.predict(X_test_3)

mostrar_resultados(y_test, y_pred)
saca_metricas(y_test, y_pred)

score_2 = classifier_2.score(X_train_3,y_train)
              precision    recall  f1-score   support

           0       0.99      0.98      0.99     17677
           1       0.77      0.91      0.84      1417

    accuracy                           0.97     19094
   macro avg       0.88      0.94      0.91     19094
weighted avg       0.98      0.97      0.97     19094

AUC
0.9428440059566496

Por lo visto al diferir sólo en una variable la precisión aduraspenas llega a afectarse

In [137]:
score = classifier_2.score(X_train_3,y_train)
 
print("Metrica del modelo", score)

accuracy_score(y_test, y_pred)
Metrica del modelo 1.0
Out[137]:
0.97329003875563
In [141]:
score = classifier_2.score(X_train_3,y_train)
 
#print("Metrica del modelo", score)

#recall_score(y_test, y_pred)

Eliminación recursivas de Características con validación cruzada y RandomForest

Con este método no solo encontraré las mejores características, sino también cuántas necesitaré para una mayor precisión. http://scikit-learn.org/stable/modules/generated/sklearn.feature_selection.RFECV.html

In [68]:
# La puntuación de "precisión" es proporcional al número de clasificaciones correctas
classifier_3 = RandomForestClassifier() 
rfecv = RFECV(estimator=classifier_3, step=1, cv=10,scoring='accuracy')   #5-fold cross-validation
rfecv = rfecv.fit(X_train, y_train)

print('Optimal number of features :', rfecv.n_features_)
print('Best features :', X_train.columns[rfecv.support_])
Optimal number of features : 12
Best features : Index(['edad', 'facturacion', 'incidencia', 'num_llamad_ent', 'num_llamad_sal',
       'mb_datos', 'seg_llamad_ent', 'seg_llamad_sal', 'financiacion',
       'descuentos', 'InfoNumdt', 'CCAA'],
      dtype='object')
In [69]:
# Plot number of features VS. cross-validation scores
import matplotlib.pyplot as plt
plt.figure()
plt.xlabel("Number of features selected")
plt.ylabel("Cross validation score of number of selected features")
plt.plot(range(1, len(rfecv.grid_scores_) + 1), rfecv.grid_scores_)
plt.show()

A medida que aumenta el número de variables aumenta la precisión del modelo.

Selección de características basadas en RandomForest

In [70]:
clf_rf_5 = RandomForestClassifier()      
clr_rf_5 = clf_rf_5.fit(X_train,y_train)
importances = clr_rf_5.feature_importances_
std = np.std([tree.feature_importances_ for tree in clf_rf_5.estimators_],
             axis=0)
indices = np.argsort(importances)[::-1]

# Print the feature ranking
print("Feature ranking:")

for f in range(X_train.shape[1]):
    print("%d. feature %d (%f)" % (f + 1, indices[f], importances[indices[f]]))

# Plot the feature importances of the forest

plt.figure(1, figsize=(14, 13))
plt.title("Feature importances")
plt.bar(range(X_train.shape[1]), importances[indices],
       color="g", yerr=std[indices], align="center")
plt.xticks(range(X_train.shape[1]), X_train.columns[indices],rotation=90)
plt.xlim([-1, X_train.shape[1]])
plt.show()    
Feature ranking:
1. feature 14 (0.368586)
2. feature 3 (0.294561)
3. feature 13 (0.094753)
4. feature 12 (0.029484)
5. feature 7 (0.024923)
6. feature 8 (0.024856)
7. feature 6 (0.024782)
8. feature 1 (0.024713)
9. feature 4 (0.023312)
10. feature 5 (0.020765)
11. feature 0 (0.019390)
12. feature 15 (0.013698)
13. feature 10 (0.011835)
14. feature 16 (0.008184)
15. feature 2 (0.008064)
16. feature 11 (0.005027)
17. feature 9 (0.003065)

1*f0bLwuRcQL8-mmDvQiLLEA>

In [371]:
x_labels = ('Algorithm','Precision','Recall','Overal')
y_labels = ('Modelo_1','Modelo_2')
score_array = np.array([['RandomForest',precision_score(y_test, y_pred_1), 
                         recall_score(y_test, y_pred_1), f1_score(y_test, y_pred_1)],
                      ['RandomForest',precision_score(y_test, y_pred), 
                       recall_score(y_test, y_pred), f1_score(y_test, y_pred)]])  
fig = plt.figure(1)
fig.subplots_adjust(left=0.002,top=0.8, wspace=1)
ax = plt.subplot2grid((4,3), (1,1), colspan=2, rowspan=2)
score_table = ax.table(cellText=score_array,
                       rowLabels=y_labels,
                       colLabels=x_labels,
                       loc=
                       'upper center')
score_table.set_fontsize(22)
ax.axis("off") # Hide plot axis
fig.set_size_inches(w=25, h=13)
plt.title('Comparación de Modelos Iniciales', fontdict = {'fontsize' : 20})
plt.show()

x_labels = ('Accuracy','Score')
y_labels = ('Modelo_1','Modelo_2')
score_array = np.array([[accuracy_score(y_test, y_pred_1),score_1],
                        [accuracy_score(y_test, y_pred),score_2]])  
fig = plt.figure(1)
fig.subplots_adjust(left=0.002,top=0.8, wspace=1)
ax = plt.subplot2grid((4,3), (1,1), colspan=2, rowspan=2)
score_table = ax.table(cellText=score_array,
                       rowLabels=y_labels,
                       colLabels=x_labels,
                       loc=
                       'upper center')
score_table.set_fontsize(22)
ax.axis("off") # Hide plot axis
fig.set_size_inches(w=25, h=13)
plt.title('Comparación de Modelos Iniciales', fontdict = {'fontsize' : 20})
plt.show()

Conclusión Modelo 1

1) Tipo de Algoritmo Utilizado: RandomForest Al análisis de distribución de clases inicial donde se presenta un desequilibrio de clases con un sesgo hacía la etiqueta de clase permanencia cuya clase es mayoritaria en aproximandamente un 93% con una gran diferencia sobre la clase minoritaria cuya representación es del 7 % ,en general esto afecta a los algoritmos en su proceso de generalización de la información y perjudica a las clases minoritarias al no lograr diferenciar de una clase a otra y se limite a beneficiar siempre a la clase mayoritaria. Por ello decidí trabajar inicialmente con el algoritmo RandomForest el cual No es superable en precisión, de entre los algoritmos actuales, y mediante una combinación de árboles de decisión mejora la precisión en la clasificación mediante la incorporación de aleatoriedad en la construcción de cada clasificador individual y a su vez aporta estimaciones de qué variables son importantes en la clasificación.

2) Varibles Seleccionadas: Con el fin de mejorar la capacidad predictiva de mi modelo así como reducir su complejidad acudí a las tecnicas detalladas anteriormente evitando el no seleccionar predictores que podrían ser importantes e introducir sesgo en mi modelo,o todo lo contrarío obtener un modelo excesivamente especificado con variables predictoras redundantes y con estimaciones poco precisas.

En el proceso de selección de atributos las 5 variables más relevantes fuerón InfoNumdt,incidencia,descuentos,seg_llamad_sal y mb_datos. Para tener un modelo más simple y mucho mas ameno de explicar decidido incluir en mi modelo 8 variables que en su mayoría 3 de ellas están relacionadas con el consumo (InfoNumdt,incidencia,descuentos,seg_llamad_sal,seg_llamad_ent,mb_datos,financiación y facturación) lo que se reduce a una mejor comprensión de los problemas que quiero abordar y un mejor conocimiento de dominio del negocio en cuestión en cuanto a las estrategías comerciales que deberán abordar en telefóníca.

3) Métricas Obtenidas Explicación Modelo:

Precisión: Mi modelo tiene una precisión del 78% en la No permanencía de los clientes es decir,cuando mi modelo hace una predicción, la frecuencia con la que es correcto . Al responder a la pregunta ¿Que cantidad de clientes se irán de la compañía?, acertará un 78% y se equivocará un 22% de las veces cuando prediga que un cliente se irá de la compañía.Por lo tanto la calidad del modelo aparentemente es buena.

Recall(Exhaustividad): Está métrica me informa sobre la cantidad real que el modelo es capaz de identificar, por tanto la exhaustividad es la respuesta a ¿que porcentaje de los clientes que se irán de la compañía somos capaces de identificar?

Es decir, el modelo es capaz de identificar correctamente un 91% de los clientes que decidirán No permanecer en la compañía. Esto significa que el modelo podría identifica 6 de cada 7 clientes que se marcharán de telefónica.

F1:El valor F1 se utiliza para combinar las medidas de precision y recall (precisión y sensibilidad en una sola métrica.)en un sólo valor. lo que hace más fácil el poder comparar el rendimiento combinado de la precisión y la exhaustividad entre varias soluciones. Mi modelo al tener alta precisión y bajo recall es decir mi modelo no detecta la clase muy bien pero cuando lo hace es altamente confiable. Al tener un dataset con desequilibrio, suele ocurrir que se obtiene un alto valor de precisión en la clase Mayoritaria y un bajo recall en la clase Minoritaria.

Accuracy(Exactitud):Está métrica mide el porcentaje de casos en que mi modelo ha acertado es decir un 97% aunque está métrica es de mucho cuidado puede hacer que un modelo malo parezca que es mucho mejor de lo que es. por ello este modelo necesitará algunas otras validaciones. Como el accuracy en Test es cercano al conjunto de entrenamiento quiere decir que el modelo entrenado está bien generalizado de no ser así podría ser un indicador de Overfitting.

Planteamiento Problema: Ante una precisión del 78% y Exhaustividad del 91% los falsos positivos aumentan y los falsos negativos disminuyen. Como resultado, esta vez la precisión disminuye y aumenta la exhaustividad. Al tratar con desbalance de clases es comveniente mayor Exhaustividad ya que nos interesa incrementar las clasificaciones correctas en la clase 2 (que son los clientes en riesgo de fuga de la compañía) por tanto se debe minimizar a toda costa un incremento en el error de Tipo II, la compañía perderá mucho dinero sino hay una detección temprana de los clientes que se fugaran y no podrían implementar estrategías comerciales a tiempo y esto indudablemente reduce sus posibilidades de recuperar al cliente en cuestión. En otras palabras detectar a tiempo cualquier síntoma que indique una posible fuga de clientes es determinante para la capacidad de reacción de las empresas porque una vez se han ido a la competencia, recuperarlos es extremadamente complejo y costoso económicamente Hay que tener en cuenta que es más difícil y caro captar clientes nuevos o tratar de recuperar clientes ya perdidos que retener a los clientes actuales.

Conclusión:

-Con el planteamiento del problema en cuestión y las métricas obtenidas decido como métrica de Negocio el recall, como lo explique anteriormente.

-Al tratarse de un conjunto de datos en desequilibrio la precisión de clasificación puede ser engañosa por tanto tendré que verificar si mejora con un ajuste de hiperparámetros con algún tipo de penalización. Hay que tener encuenta que el entrenamiento y validación es sobre el mismo conjunto de datos, por tanto los algoritmos ya se han entrenado sobre un conjunto de datos generalizado en test, la prueba de fuego será cuando intenten predecir datos nuevos (Cosecha Enero) a partir de crear un nuevo modelo entrenando con todos los datos del dataset diciembre.

La precisión no es la métrica a utilizar cuando se trabaja con un conjunto de datos desequilibrado , por tanto debo fijarme en medidas de integridad de un clasificador o en el promedio ponderado de precisión y recuperación.

y como se puede ver en la matriz de confusión hace pocas predicciones erróneas. Los Falsos negativos son 207(es decir 207 clientes que se predicen erróneamente como clientes que permanecerán en la compañía cuando en realidad no es así) en comparación con los falsos positivos que son 339(es decir 339 clientes que no se irán de la compañía fuerón predecidos erróneamente como clientes que Si se marcharán), en cuanto a las 17338 observaciones son los clientes que permanecerán en la compañía clasificados correctamente y 1210 observaciones clasificadas correctamente como clientes que no permanecerán en la compañía.

In [29]:
X_train = X_train.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
                       'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
X_test = X_test.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
                       'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)

X_test_analysis = X_test_analysis.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
                       'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)

X_train_analysis = X_train_analysis.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
                       'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)

df_train = df_train.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
                       'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)
df_test = df_test.drop(['num_llamad_ent','num_llamad_sal','edad','diff_Month',
                       'CCAA','vel_conexion','num_lineas','TV','conexion'], axis=1)

Normalización Features Continuas

In [30]:
#####
X_train[['mb_datos','seg_llamad_ent',
          'seg_llamad_sal','facturacion']] = X_train[['mb_datos','seg_llamad_ent',
                                         'seg_llamad_sal','facturacion']].apply(zscore)

X_test[['mb_datos','seg_llamad_ent',
          'seg_llamad_sal','facturacion']] = X_test[['mb_datos','seg_llamad_ent',
                                         'seg_llamad_sal','facturacion']].apply(zscore)
## conjunto de validación
X_test_analysis[['mb_datos','seg_llamad_ent',
          'seg_llamad_sal','facturacion']] = X_test_analysis[['mb_datos','seg_llamad_ent',
                                         'seg_llamad_sal','facturacion']].apply(zscore)

X_train_analysis[['mb_datos','seg_llamad_ent',
          'seg_llamad_sal','facturacion']] = X_train_analysis[['mb_datos','seg_llamad_ent',
                                         'seg_llamad_sal','facturacion']].apply(zscore)
#### 
df_train[['mb_datos','seg_llamad_ent',
          'seg_llamad_sal','facturacion']] = df_train[['mb_datos','seg_llamad_ent',
                                         'seg_llamad_sal','facturacion']].apply(zscore)
df_test[['mb_datos','seg_llamad_ent',
          'seg_llamad_sal','facturacion']] = df_test[['mb_datos','seg_llamad_ent',
                                         'seg_llamad_sal','facturacion']].apply(zscore)
In [31]:
X_test_analysis.head(7)
Out[31]:
facturacion incidencia mb_datos seg_llamad_ent seg_llamad_sal financiacion descuentos InfoNumdt
id
1 0.076766 0 -0.829246 -0.407175 1.467075 1 1 0
2 0.434580 0 0.271669 0.788869 -0.348115 1 0 0
3 -0.644744 0 -1.535433 -0.820344 -0.505371 1 0 0
4 0.433031 0 1.049210 -0.452544 -0.860322 1 1 0
6 -0.972143 0 0.909967 -0.861730 -0.232333 1 1 0
7 -1.073598 0 -1.313199 -0.842856 0.287651 0 1 0
8 -1.208321 0 0.544751 -0.383451 0.595770 1 1 0
In [32]:
X_train_analysis.head(7)
Out[32]:
facturacion incidencia mb_datos seg_llamad_ent seg_llamad_sal financiacion descuentos InfoNumdt
id
1 0.077555 0 -0.220018 0.489592 0.645657 1 1 0
2 0.435034 0 0.851896 -0.604774 0.144398 1 0 0
3 -0.643281 0 0.417329 1.219575 1.164787 1 0 0
4 0.433486 0 0.024892 -1.143716 -0.565415 1 1 0
5 -1.662343 0 1.556238 1.466488 -0.962050 1 1 1
6 -0.970374 0 0.820264 -0.178445 -0.913641 1 1 0
7 -1.071734 0 -1.714206 1.114250 1.694675 0 1 0

Entrenando diferentes Algoritmo con las variables Seleccionadas.

k-Nearest Neighbors algorithm

Crearé un clasificador usando el algoritmo k-Nearest Neighbours. Primero observaré las precisiones para diferentes valores de k.

In [160]:
#Setup arrays to store training and test accuracies
neighbors = np.arange(1,9)
train_accuracy =np.empty(len(neighbors))
test_accuracy = np.empty(len(neighbors))

for i,k in enumerate(neighbors):
    #Setup a knn classifier with k neighbors
    knn = KNeighborsClassifier(n_neighbors=k)
    
    #Fit the model
    knn.fit(X_train, y_train)
    
    #Compute accuracy on the training set
    train_accuracy[i] = knn.score(X_train, y_train)
    
    #Compute accuracy on the test set
    test_accuracy[i] = knn.score(X_test, y_test) 
In [231]:
plt.title('k-NN Varying number of neighbors')
plt.plot(neighbors, test_accuracy, label='Testing Accuracy')
plt.plot(neighbors, train_accuracy, label='Training accuracy')
plt.legend()
plt.xlabel('Number of neighbors')
plt.ylabel('Accuracy')
plt.show()

Se tiene la máxima precisión en test a partir de k = 7. Entonces, crearé un KNeighboursclassifier con un número de vecinos igual a 7.

In [162]:
knn = KNeighborsClassifier(n_neighbors=7)
knn=knn.fit(X_train, y_train)
y_pred_knn1 = knn.predict(X_test)
mostrar_resultados(y_test, y_pred_knn1)
              precision    recall  f1-score   support

           0       1.00      0.98      0.99     17677
           1       0.82      0.98      0.89      1417

    accuracy                           0.98     19094
   macro avg       0.91      0.98      0.94     19094
weighted avg       0.99      0.98      0.98     19094

In [163]:
y_pred_knn1 = knn.predict(X_test_analysis)
y_prob_knn1 = knn.predict_proba(X_test_analysis)
In [165]:
from funcion_proba import clientes_proba
clientes_proba(y_pred_knn1, y_prob_knn1 , "KNeighborsClassifier_1")
In [179]:
def run_model_balanced(X_Train,y_Train):
    clf = LogisticRegression(C=0.5,penalty='l2',random_state=0,solver="sag",
                             class_weight="balanced")
    clf.fit(X_Train,y_Train)
    return clf

model_balc = run_model_balanced(X_train, y_train)
y_pred_balc = model_balc.predict(X_test_analysis)
y_proba_balc = model_balc.predict_proba(X_test_analysis)
In [180]:
clientes_proba(y_pred_balc, y_proba_balc, "LogisticRegressionPenalización")
In [185]:
forest = RandomForestClassifier(n_estimators = 500, criterion = 'entropy', random_state = 0)
forest.fit(X_train, y_train)
y_pred_test_for = forest.predict(X_test_analysis)
y_prob_test_for  = forest.predict_proba(X_test_analysis)
In [186]:
clientes_proba(y_pred_test_for, y_prob_test_for , "RForest")

En total 1547 clientes según el modelo estimado con RandomForest se irán de la compañía 17 clientes tienes una probabilidad del 50% de marcharse de la compañía de esos 17 clientes 15 fuerón clasificados en la clase 2 (no permanencia) y 2 de ellos en la clase 1, esta variación en class_predict ocurre por el redondeo en las probabilidades para poder agrupar los clientes.

Dummy Variables Encoding

Los algoritmos de aprendizaje automático no pueden procesar texto y variables categóricas, a menos que tengan una función incorporada. Entonces tenemos que convertir las variables categóricas en variables numéricas. Pero codificarlos en valores ordinales podría hacer que el algoritmo se sesgue sobre qué tan grandes son los números, es por eso que lo haré de ambas formas.

In [188]:
x_train = pd.get_dummies(df_train, columns=['incidencia','financiacion','descuentos','InfoNumdt'])
x_test = pd.get_dummies(df_test, columns=['incidencia','financiacion','descuentos','InfoNumdt'])
In [189]:
x_train.columns
Out[189]:
Index(['facturacion', 'mb_datos', 'seg_llamad_ent', 'seg_llamad_sal',
       'incidencia_NO', 'incidencia_SI', 'financiacion_NO', 'financiacion_SI',
       'descuentos_NO', 'descuentos_SI', 'InfoNumdt_0', 'InfoNumdt_1'],
      dtype='object')

La multicolinealidad es lo que queremos evitar al usar variables ficticias. Cuando 2 o más variables están fuertemente correlacionadas entre sí, por ejemplo, la función 'descuentos' cuando se convierte se convierte en variables ficticias descuento_SI y descuento_NO. Cuando descuento_NO= 0, descuento_SI siempre es 1 y viceversa. En otras palabras, tenemos variables duplicadas que siempre son diferentes. Esta situación también se llama "Trampa variable variable simulada".

Podemos evitar esto excluyendo cualquier 1 ficticio para cada característica. En este caso, elegí eliminar la primera variable ficticia de cada característica.

In [190]:
x_train = x_train.drop(['incidencia_NO','financiacion_SI','descuentos_SI',
                          'InfoNumdt_0',], axis=1)
x_test = x_test.drop(['incidencia_NO','financiacion_SI','descuentos_SI',
                         'InfoNumdt_0',], axis=1)
In [192]:
x_train.shape
Out[192]:
(95467, 8)
In [212]:
knn = KNeighborsClassifier(n_neighbors=9)
knn=knn.fit(x_train, target)
y_pred_kndum = knn.predict(x_test)
y_prob_kndum = knn.predict_proba(x_test)
In [213]:
clientes_proba(y_pred_kndum , y_prob_kndum , "KNeighborsClassifier_2")

4. Machine Learning

Fundamental

Hay 3 opciones para probar la precisión de cada clasificador para ver cuál tiene la mejor precisión y la menor varianza.

  1. Usar el conjunto de entrenamiento para entrenar e intentar predecir el mismo conjunto de entrenamiento.

  2. Dividir el nuevo conjunto de entrenamiento y el conjunto de prueba del conjunto de entrenamiento actual (en este caso, X_train e y_train).

  3. Utilizar la técnica de validación cruzada k-fold.

La primera opción tiende a ofrecer una alta precisión, pero no porque el modelo sea bueno, sino porque el modelo está tratando de predecir los datos, lo que ya ha visto. Esta opción debe evitarse a toda costa.

La segunda opción es bastante simple de hacer usando la biblioteca train_test_split, pero el problema es que a veces el conjunto de entrenamiento y el conjunto de datos no se dividen de manera uniforme y nos dan datos muy sesgados. Por ejemplo, un nuevo conjunto de entrenamiento con Num_líneas= 1 y 2 pero no tiene Num_Líneas = 3 en absoluto. Lo que podría hacernos juzgar mal sobre qué modelo es el mejor.

La tercera opción, k-fold Cross Validation, divide los datos de entrenamiento en k divisiones, luego entrena y realiza predicciones k veces. Esta técnica nos ayuda a reducir la posibilidad de sobreajuste y datos sesgados.

A continuación se detallan los procedimientos para utilizar la validación cruzada de k-fold. A partir de aquí crearé cada modelo en base a todo el conjunto de diciembre.

Finding the Best Classifier by Cross Validation

A continuación, voy a entrenar y validar estos modelos de clasificación.

  1. Logistic Regression
  2. K-Nearest Neighbors
  3. Bayes ingenuos
  4. Decision Tree
  5. Random Forest

Logistic Regression

In [215]:
logreg = LogisticRegression()
logreg.fit(X_train_analysis,target)
acc_logreg = cross_val_score(estimator = logreg, X = X_train_analysis, y = target, cv = 10)
logreg_acc_mean = acc_logreg.mean()
logreg_std = acc_logreg.std() 

K-Nearest Neighbors

n_neighbours es el número de vecinos, que utiliza el algoritmo para decidir qué clasificación se debe asignar a cada dato. señale n_neighbours = 5, que es el valor predeterminado.

metric = 'minkowski' y p = 2 ahora estoy usando la distancia euclidiana para juzgar la distancia entre los datos.

In [216]:
knn = KNeighborsClassifier(n_neighbors = 5, metric = 'minkowski', p = 2 ) 
knn.fit(X_train_analysis,target)
acc_knn = cross_val_score(estimator = knn, X = X_train_analysis, y = target, cv = 10)
knn_acc_mean = acc_knn.mean()
knn_std = acc_knn.std()

Kernel SVM

In [217]:
ksvm = SVC(kernel = 'rbf', random_state = 0)
ksvm.fit(X_train_analysis,target)
acc_ksvm = cross_val_score(estimator = ksvm, X = X_train_analysis, y = target, cv = 10)
ksvm_acc_mean = acc_ksvm.mean()
ksvm_std = acc_ksvm.std()

Naive Bayes

In [218]:
naive = GaussianNB()
naive.fit(X_train_analysis,target)
acc_naive = cross_val_score(estimator = naive, X = X_train_analysis, y = target, cv = 10)
naive_acc_mean = acc_naive.mean()
naive_std = acc_naive.std()

Decision Tree

In [219]:
dtree = DecisionTreeClassifier(criterion = 'gini', random_state = 0)
dtree.fit(X_train_analysis,target)
acc_dtree = cross_val_score(estimator = dtree, X = X_train_analysis, y = target, cv = 10)
dtree_acc_mean = acc_dtree.mean()
dtree_std = acc_dtree.std()

Random Forest

In [220]:
rforest = RandomForestClassifier(n_estimators = 10, criterion = 'gini', random_state = 0)
rforest.fit(X_train_analysis,target)
acc_rforest = cross_val_score(estimator = rforest, X = X_train_analysis, y = target, cv = 10)
rforest_acc_mean = acc_rforest.mean()
rforest_std = acc_rforest.std()

XGBoost

In [221]:
xgb = XGBClassifier()
xgb.fit(X_train_analysis,target)
acc_xgb = cross_val_score(estimator = xgb, X = X_train_analysis, y = target, cv = 10)
xgb_acc_mean = acc_xgb.mean()
xgb_std = acc_xgb.std()

Cross Validation Score

In [222]:
x_labels = ('Accuracy','Deviation')
y_labels = ('Logistic Regression','K-Nearest Neighbors','Kernel SVM','Naive Bayes'
            ,'Decision Tree','Random Forest','XGBoost')
score_array = np.array([[logreg_acc_mean, logreg_std],
                        [knn_acc_mean, knn_std],
                        [ksvm_acc_mean, ksvm_std],
                        [naive_acc_mean, naive_std],
                        [dtree_acc_mean, dtree_std],
                        [rforest_acc_mean, rforest_std],
                        [xgb_acc_mean, xgb_std]])  
fig = plt.figure(1)
fig.subplots_adjust(left=0.2,top=0.8, wspace=1)
ax = plt.subplot2grid((4,3), (0,0), colspan=2, rowspan=2)
score_table = ax.table(cellText=score_array,
                       rowLabels=y_labels,
                       colLabels=x_labels,
                       loc='upper center')
score_table.set_fontsize(14)
ax.axis("off") # Hide plot axis
fig.set_size_inches(w=18, h=10)
plt.show()

Debido a cómo funciona la validación cruzada k-fold, el puntaje de precisión y la desviación estándar para cada ejecución pueden cambiar ligeramente. Por lo tanto, el valor de puntuación que escribí aquí puede no ser exacto, pero tampoco cambiará tanto.

En nuestra ejecución, encontramos que XGBoost tiene la mejor precisión al 98% y la desviación estándar al 0.09% seguido de regresión logística.

Para Naive Bayes,entre todos los algoritmos tiene la precisión mas baja.

Hyper Parameters Tuning by Grid Search

Hasta ahora,en los procesos de aprendizaje automático solo utilicé los hiperparámetros predeterminados o seleccionados personalmente (parámetros que podrían cambiarse manualmente, por ejemplo, en Random Forest, n_estimators y criterios). Podría intentar cambiar estos hiperparámetros y volver a ejecutarlos para verificar la precisión. Pero este proceso, cuando se repite varias veces, es realmente lento, por lo que use Grid Search para ayudar a probar diferentes hiperparámetros a la vez.

Logistics Regression

Los hiperparámetros que podrían ajustarse para mejorar la precisión de la regresión logística son ...

  1. C
  2. penalty
In [233]:
params_logreg = [{'C': [0.01, 0.1, 1, 10, 100], 'penalty': ['l1','l2']}]
grid_logreg = GridSearchCV(estimator = LogisticRegression(),
                           param_grid = params_logreg,
                           scoring = 'accuracy',
                           cv = 10)
grid_logreg = grid_logreg.fit(X_train_analysis,target)
best_acc_logreg = grid_logreg.best_score_
best_params_logreg = grid_logreg.best_params_

K-Nearest Neighbors

  1. n_neighbors (k)
  2. metric
  3. p (leave it at 2 which is a default value)

Estudié sobre k-NN de muchas fuentes y descubrí que la distancia de Hamming es realmente buena para los datos binarios (y datos categóricos cuando se convierten en variables ficticias), por lo que esta es una buena oportunidad para intentar cambiar la métrica de distancia.

In [234]:
params_knn = [{'n_neighbors': [7,9,10,11,12,15,20], 'metric': ['minkowski','hamming']}]
grid_knn = GridSearchCV(estimator = KNeighborsClassifier(),
                        param_grid = params_knn,
                        scoring = 'accuracy',
                        cv = 10)
grid_knn = grid_knn.fit(X_train_analysis,target)
best_acc_knn = grid_knn.best_score_
best_params_knn = grid_knn.best_params_

Kernel SVM

De acuerdo con este link Grid Search en kernel SVM tomará absurdamente una gran cantidad de tiempo de proceso si no se normaliza los datos con las características continuas primero.

In [235]:
X_train_norm = X_train_analysis.copy()
X_train_norm[['incidencia',
              'financiacion',
              'descuentos',
              'InfoNumdt']] = X_train_norm[['incidencia', 
                                              'financiacion',
                                              'descuentos', 
                                              'InfoNumdt']].apply(zscore)
X_train_norm.head()
Out[235]:
facturacion incidencia mb_datos seg_llamad_ent seg_llamad_sal financiacion descuentos InfoNumdt
id
1 0.077555 -0.240794 -0.220018 0.489592 0.645657 0.267431 0.500992 -0.270677
2 0.435034 -0.240794 0.851896 -0.604774 0.144398 0.267431 -1.996041 -0.270677
3 -0.643281 -0.240794 0.417329 1.219575 1.164787 0.267431 -1.996041 -0.270677
4 0.433486 -0.240794 0.024892 -1.143716 -0.565415 0.267431 0.500992 -0.270677
5 -1.662343 -0.240794 1.556238 1.466488 -0.962050 0.267431 0.500992 3.694444
In [ ]:
#params_ksvm = [{'C': [0.1, 1, 10, 100], 'kernel': ['linear']},
 #              {'C': [0.1, 1, 10, 100], 'kernel': ['rbf'],
  #              'gamma': [0.1, 0.2, 0.3, 0.4, 0.5]},
   #            {'C': [0.1, 1, 10, 100], 'kernel': ['poly'],
    #            'degree': [1, 2, 3],
     #           'gamma': [0.1, 0.2, 0.3, 0.4, 0.5]}]
#grid_ksvm = GridSearchCV(estimator = SVC(random_state = 0),
 #                        param_grid = params_ksvm,
  #                       scoring = 'accuracy',
   #                      cv = 10,
    #                     n_jobs=-1)
#grid_ksvm = grid_ksvm.fit(X_train_norm, y_train)  # Replace X_train with normalized version here
#best_acc_ksvm = grid_ksvm.best_score_
#best_params_ksvm = grid_ksvm.best_params_

Los parámetros para ajustar en Kernel SVM son los siguientes

  1. C
  2. kernel
  3. degree (only for poly kernel)
  4. gamma (only for rbf, poly and sigmoid kernel

Cada interior {} es una rama de parámetros que estamos entrenando. Por ejemplo, en la primera rama se está probando un kernel lineal con un valor de C diferente. La segunda rama que estoy probando es kernel rbf con diferentes C y gamma. La tercera rama que estoy tratando con diferentes C, grados y gamma. De esta manera se puede evitar el uso de parámetros innecesarios, en este caso, grado y gamma en el kernel lineal.

Tardo demasiado tiempo su ejecución por tanto no entrenaré ningún modelo con este algoritmo.

Naive Bayes

El único algoritmo con muy baja precisión en la prueba de validación cruzada anterior. Pero lamentablemente, no pude encontrar ningún hiperparámetro para sintonizar en esta clase.

Decision Tree

Según LongYiny también mis propias pruebas. min_samples_split, min_samples_leaf y max_features también son importantes en Random Forest y Decision Tree.

In [236]:
params_dtree = [{'min_samples_split': [5, 10, 15, 20],
                 'min_samples_leaf': [1, 2, 3],
                 'max_features': ['auto', 'log2']}]
grid_dtree = GridSearchCV(estimator = DecisionTreeClassifier(criterion = 'gini', 
                                                             random_state = 0),
                            param_grid = params_dtree,
                            scoring = 'accuracy',
                            cv = 10,
                            n_jobs=-1)
grid_dtree = grid_dtree.fit(X_train_analysis,target)
best_acc_dtree = grid_dtree.best_score_
best_params_dtree = grid_dtree.best_params_

Random Forest

igual Decision Tree, pero también con n_estimators.

In [237]:
params_rforest = [{'n_estimators': [100,200, 300,500],
                   'max_depth': [5, 10, 15, 20],
                   'min_samples_split': [1,2,3,4]}]
grid_rforest = GridSearchCV(estimator = RandomForestClassifier(criterion = 'gini', 
                                                               random_state = 0,
                                                               n_jobs=-1),
                            param_grid = params_rforest,
                            scoring = 'accuracy',
                            cv = 10,
                            n_jobs=-1)
grid_rforest = grid_rforest.fit(X_train_analysis,target)
best_acc_rforest = grid_rforest.best_score_
best_params_rforest = grid_rforest.best_params_

Grid Search Score

In [238]:
grid_score_dict = {'1. Grid Search Score': [best_acc_logreg,best_acc_knn,'-','-',
                                            best_acc_dtree,best_acc_rforest,'(add later)'],
                   '2. Previous Score': [logreg_acc_mean,knn_acc_mean,'-',naive_acc_mean,
                                         dtree_acc_mean,rforest_acc_mean,xgb_acc_mean],
                   '3. Optimized Parameters': [best_params_logreg,best_params_knn,'-','-',
                                               best_params_dtree,best_params_rforest,'(add later)'],
                  }
pd.DataFrame(grid_score_dict, index=['Logistic Regression','K-Nearest Neighbors','Kernel SVM','Naive Bayes',
                                     'Decision Tree','Random Forest','XGBoost'])
Out[238]:
1. Grid Search Score 2. Previous Score 3. Optimized Parameters
Logistic Regression 0.983638 0.983596 {'C': 100, 'penalty': 'l2'}
K-Nearest Neighbors 0.983743 0.981219 {'metric': 'hamming', 'n_neighbors': 15}
Kernel SVM - - -
Naive Bayes - 0.91789 -
Decision Tree 0.976358 0.972367 {'max_features': 'auto', 'min_samples_leaf': 3...
Random Forest 0.983743 0.980297 {'max_depth': 10, 'min_samples_split': 3, 'n_e...
XGBoost (add later) 0.983743 (add later)
In [243]:
#Guardando modelos (Fuerón muy pesados al ejecutarse)
#joblib.dump(grid_logreg, 'modelo_grid_logreg')
#joblib.dump(grid_knn, 'modelo_grid_knn')
#joblib.dump(grid_dtree, 'modelo_grid_dtree')
#joblib.dump(grid_rforest, 'modelo_grid_rforest')

#Cargando Modelos
#grid_logreg = joblib.load('modelo_grid_logreg')
#grid_knn = joblib.load('modelo_grid_knn')
#grid_dtree = joblib.load('modelo_grid_dtree')
#grid_rforest= joblib.load('modelo_grid_rforest')
Out[243]:
['modelo_grid_rforest']

Debido a que toma demasiado tiempo procesarlo, pongo una lista completa de parámetros para Random Forest aquí, mientras reduzco los parámetros y valores anteriores, pero aún incluyo el mejor.

In [ ]:
""" params_rforest = [{'n_estimators': [100, 200, 500, 800], 
                   'min_samples_split': [5, 10, 15, 20],
                   'min_samples_leaf': [1, 2, 3],
                   'max_features': ['auto', 'log2']}] """
In [244]:
best_params_logreg 
Out[244]:
{'C': 100, 'penalty': 'l2'}
In [245]:
best_params_knn
Out[245]:
{'metric': 'hamming', 'n_neighbors': 15}
In [246]:
best_params_dtree
Out[246]:
{'max_features': 'auto', 'min_samples_leaf': 3, 'min_samples_split': 20}
In [247]:
best_params_rforest
Out[247]:
{'max_depth': 10, 'min_samples_split': 3, 'n_estimators': 100}

Train the Algorithms with Optimized Parameters

Después de entrenar los modelos, mantengo tanto la precisión de predicción en el conjunto de datos de entrenamiento a través de la validación cruzada (y_pred_train) como la predicción en el conjunto de datos de prueba (y_pred_test) para usar en la siguiente sección.

In [248]:
logreg = LogisticRegression(C = 100, penalty = 'l2')
logreg.fit(X_train_analysis, target)   
y_pred_train_logreg = cross_val_predict(logreg, X_train_analysis, target)
y_pred_test_logreg = logreg.predict(X_test_analysis)
In [33]:
logreg_1 = LogisticRegression(C = 1, penalty = 'l2')
logreg_1.fit(X_train_analysis, target)   
y_pred_train_logreg_1 = cross_val_predict(logreg_1, X_train_analysis, target)
y_pred_test_logreg_1 = logreg_1.predict(X_test_analysis)
In [250]:
knn = KNeighborsClassifier(n_neighbors = 15, metric = "hamming")
knn.fit(X_train_analysis, target)   
y_pred_train_knn = cross_val_predict(knn, X_train_analysis, target)
y_pred_test_knn = knn.predict(X_test_analysis)
In [251]:
dtree = DecisionTreeClassifier(criterion = 'gini', max_features='auto', min_samples_leaf=3, min_samples_split=20,
                               random_state = 0)
dtree.fit(X_train_analysis, target)
y_pred_train_dtree = cross_val_predict(dtree, X_train_analysis, target)
y_pred_test_dtree = dtree.predict(X_test_analysis)
In [252]:
rforest = RandomForestClassifier(max_depth = 10, min_samples_split=3, n_estimators = 100, random_state = 0) # Grid Search best parameters
rforest.fit(X_train_analysis, target)
y_pred_train_rforest = cross_val_predict(rforest, X_train_analysis, target)
y_pred_test_rforest = rforest.predict(X_test_analysis)

Visualización Matriz de Confusión

In [ ]:
#y_test = np.array(y_test)
#class_names = np.unique(y_test)
#confusion_matrices = [
 #      ("R Logística",confusion_matrix(y_test,y_pred_test_logreg)),
  #     ("Tree",confusion_matrix(y_test, y_pred_test_dtree)),
   #    ("Random Forest",confusion_matrix(y_test, y_pred_test_rforest))
#]

# Pyplot code not included to reduce clutter
#draw_confusion_matrices(confusion_matrices,class_names)

Thinking in Probabilities

In [254]:
y_prob_test_logreg_1 = logreg_1.predict_proba(X_test_analysis)
y_prob_test_knn = knn.predict_proba(X_test_analysis)
y_prob_test_dtree = dtree.predict_proba(X_test_analysis)
y_prob_test_rforest = rforest.predict_proba(X_test_analysis)
In [34]:
y_prob_test_logreg_1 = logreg_1.predict_proba(X_test_analysis)
In [ ]:
len(y_pred_test_logreg[0][predict_logreg]*100) y_pred_test_logreg_1
In [255]:
from funcion_proba import clientes_proba
clientes_proba(y_pred_test_logreg_1, y_prob_test_logreg_1, "LogisticRegression_1")
clientes_proba(y_pred_test_knn, y_prob_test_knn, "K-Nearest Neighbors")
clientes_proba(y_pred_test_dtree, y_prob_test_dtree, "DecisionTreeClassifier")
clientes_proba(y_pred_test_rforest, y_prob_test_rforest, "RandomForestClassifier")
In [258]:
data_= y_pred_test_knn
data_= pd.DataFrame(data_)
data_.columns = ["resul"]
data_["resul"].value_counts()
Out[258]:
0    91160
1     1551
Name: resul, dtype: int64

Ensemble Models

Aprendí esta técnica de Anisotropic. Consulte su notebook para obtener más información. Pero en mi caso, decidí intentar usar predicciones de cross_val_predict en lugar de Out-of-Folds.

In [355]:
second_layer_train = pd.DataFrame( {'Logistic Regression': y_pred_train_logreg_1.ravel(),
                                    'K-Nearest Neighbors': y_pred_train_knn .ravel(),
                                    'Decision Tree': y_pred_train_dtree .ravel(),
                                    'Random Forest': y_pred_train_rforest.ravel()
                                    } )
second_layer_train.head()

X_train_second = np.concatenate(( y_pred_train_logreg_1.reshape(-1, 1), y_pred_train_knn.reshape(-1, 1), 
                                  y_pred_train_dtree.reshape(-1, 1), y_pred_train_rforest.reshape(-1, 1)),
                                  axis=1)
X_test_second = np.concatenate(( y_pred_test_logreg_1.reshape(-1, 1), y_pred_test_knn.reshape(-1, 1), 
                                 y_pred_test_dtree.reshape(-1, 1), y_pred_test_rforest.reshape(-1, 1)),
                                 axis=1)

xgb = XGBClassifier(
        n_estimators= 800,
        max_depth= 4,
        min_child_weight= 2,
        gamma=0.9,                        
        subsample=0.8,
        colsample_bytree=0.8,
        objective= 'binary:logistic',
        nthread= -1,
        scale_pos_weight=1).fit(X_train_second,target)

y_pred_ensamble = xgb.predict(X_test_second)
y_prob_ensamble = xgb.predict_proba(X_test_second)
In [362]:
clientes_proba(y_pred_ensamble, y_prob_ensamble, "XGBClassifier")
In [357]:
xgb_1 = XGBClassifier(n_estimators= 800,
        max_depth= 4,
        min_child_weight= 2,
        gamma=0.9,                        
        subsample=0.8,
        colsample_bytree=0.8,
        random_state = 0)
xgb_1 .fit(X_train_analysis, target)
y_pred_train_xgb_1  = cross_val_predict(xgb_1, X_train_analysis, target)
y_pred_test_xgb_1  = xgb_1.predict(X_test_analysis)
y_prob_test_xgb_1  = xgb_1.predict_proba(X_test_analysis)
In [358]:
clientes_proba(y_pred_test_xgb_1 , y_prob_test_xgb_1, "XGBClassifier_1")
In [363]:
xgb_2 = XGBClassifier(random_state = 0)
xgb_2 .fit(X_train_analysis, target)
y_pred_train_xgb_2  = cross_val_predict(xgb_2, X_train_analysis, target)
y_pred_test_xgb_2  = xgb_2.predict(X_test_analysis)
y_prob_test_xgb_2  = xgb_2.predict_proba(X_test_analysis)
In [364]:
clientes_proba(y_pred_test_xgb_2 , y_prob_test_xgb_2, "XGBClassifier_2")

resampling.png

Strategies for managing Unbalanced Data

  1. Ajuste de Parámetros del modelo Ajuste de parametros ó metricas del propio algoritmo para intentar equilibrar a la clase minoritaria penalizando a la clase mayoritaria durante el entrenamiento.

  2. Modificar el Dataset Eliminar muestras de la clase mayoritaria para reducirlo e intentar equilibrar la situación.

  3. Muestras artificiales Intentar crear muestras sintéticas (no idénticas) utilizando diversos algoritmos que intentan seguir la tendencia del grupo minoritario.

  4. Balanced Ensemble Methods Utiliza las ventajas de hacer ensamble de métodos, es decir, entrenar diversos modelos y entre todos obtener el resultado final (por ejemplo «votando») pero se asegura de tomar muestras de entrenamiento equilibradas.

En este ejemplo no realice un muestreo de los datos la cosecha de diciembre ni hice una predicción con los datos de validación de la misma cosecha. Ahora intentaré hacer una misma predicción entrenando un modelo con la cosecha de diciembre y validando con la cosecha de enero.

Estrategia: Penalización para compensar

Utilizaré un parámetro adicional en el modelo de Regresión logística en donde indico weight = «balanced» y con esto el algoritmo se encargará de equilibrar a la clase minoritaria durante el entrenamiento.

In [370]:
def run_model_balanced(X_Train,y_Train):
    clf = LogisticRegression(C=1.0,penalty='l2',random_state=0,solver="newton-cg",class_weight="balanced")
    clf.fit(X_Train,y_Train)
    return clf

model_one = run_model_balanced(X_train_analysis,target)
y_pred_balanc = model_one.predict(X_test_analysis)
y_proba_balanc = model_one.predict_proba(X_test_analysis)
In [392]:
from funcion_proba import clientes_proba
In [414]:
clientes_proba(y_pred_test_xgb_2 , y_prob_test_xgb_2, "Balanced LogisticRegression")

Estrategia: Subsampling en la clase mayoritaria

Lo que haré es utilizar un algoritmo para reducir la clase mayoritaria. Lo haré usando un algoritmo que es similar al k-nearest neighbor para ir seleccionando cuales eliminar. Fijemonos que reducimos bestialmente de 88382 muestras de clase cero (la mayoría) y pasan a ser 7085 y Con esas muestras entrenamos el modelo. https://imbalanced-learn.readthedocs.io/en/stable/generated/imblearn.under_sampling.NearMiss.html

In [394]:
us = NearMiss(sampling_strategy='majority' ,n_neighbors=3, version=2)
X_train_res, y_train_res = us.fit_sample(X_train_analysis,target)
 
print ("Distribution before resampling {}".format(Counter(target)))
print ("Distribution after resampling {}".format(Counter(y_train_res)))
 
#model_two= run_model(X_train_res,y_train_res)
model_Near = run_model_balanced(X_train_res, y_train_res)
y_pred_Near = model_Near.predict_proba(X_test_analysis)
y_prob_Near = model_Near.predict_proba(X_test_analysis)
Distribution before resampling Counter({0: 88382, 1: 7085})
Distribution after resampling Counter({0: 7085, 1: 7085})
In [395]:
clientes_proba(y_pred_Near , y_prob_Near, "NearMiss")

A pesar de que se redujo considerablemente la clase mayorítaria el modelo no es capaz de predecir nada de la clase 2

Estrategia: Oversampling de la clase minoritaria

En este caso, crearé muestras nuevas «sintéticas» de la clase minoritaria. Usando RandomOverSampler. Y vemos que pasamos de 344 muestras de fraudes a 99.510.

In [397]:
os =  RandomOverSampler()
X_train_res, y_train_res = os.fit_sample(X_train_analysis,target)
 
print ("Distribution before resampling {}".format(Counter(target)))
print ("Distribution labels after resampling {}".format(Counter(y_train_res)))
 
model_three = run_model_balanced(X_train_res,y_train_res)
y_pred_Over = model_three.predict(X_test_analysis)
y_prob_Over = model_three.predict_proba(X_test_analysis)
Distribution before resampling Counter({0: 88382, 1: 7085})
Distribution labels after resampling Counter({0: 88382, 1: 88382})
In [398]:
clientes_proba(y_pred_Over, y_prob_Over, "RandomOverSampler")

Mejora considerablemente. La naturaleza de los datos responde mejor a un aumento de la clase minoritaria que a una disminución de la clase mayortaria.

Estrategia: Combinamos resampling con Smote-Tomek

Esta técnica consiste en aplicar en simultáneo un algoritmo de subsampling y otro de oversampling a la vez al dataset. En este caso usaré SMOTE para oversampling: Se encarga de buscar puntos es decir vecinos cercanos y agrega puntos «en linea recta» entre ellos. Y usaré Tomek para undersampling que quita los de distinta clase que sean nearest neighbor y deja ver mejor el decisión boundary (la zona limítrofe de nuestras clases).

In [408]:
os_us = SMOTETomek(sampling_strategy='not minority')
X_train_res, y_train_res = os_us.fit_sample(X_train_analysis,target)
 
print ("Distribution before resampling {}".format(Counter(target)))
print ("Distribution after resampling {}".format(Counter(y_train_res)))
 
model = run_model_balanced(X_train_res,y_train_res)
y_pred_SMOTE = model.predict(X_test_analysis)
y_prob_SMOTE = model.predict_proba(X_test_analysis)
Distribution before resampling Counter({0: 88382, 1: 7085})
Distribution after resampling Counter({0: 87642, 1: 6345})
In [409]:
clientes_proba(y_pred_SMOTE , y_prob_SMOTE, "Smote-Tomek")

Estrategia: Ensamble de Modelos con Balanceo

Para esta estrategia usaremos un Clasificador de Ensamble que utiliza Bagging y el modelo será un DecisionTree. Veamos como se comporta.

In [412]:
bbc = BalancedBaggingClassifier(base_estimator=DecisionTreeClassifier(),
                                sampling_strategy='auto',
                                replacement=False,
                                random_state=0)
 
#Train the classifier.
bbc.fit(X_train_analysis,target)
y_pred_bbc = bbc.predict(X_test_analysis)
y_prob_bbc = bbc.predict_proba(X_test_analysis)
In [413]:
clientes_proba(y_pred_bbc , y_prob_bbc, "BalancedBaggingClassifier")

Conclusiones

Modelo Ganador : Regresión Logística

A partir de aqui se mostrarán los resultados del modelo seleccionado como el más conveniente para predecir la fuga de clientes en una empresa de telecomunicaciones. Dado que son datos que se generarón de manera aletoria no es de esperar que los resultados obtenidos sean comparables a la realidad pero si los procedimientos de limpieza, análisis y mejora de modelos. Los principales retos consistierón en trabajar con un problema de desbalanceo de clases (existiendo una clase predominante y una minoritaria, fuga de clientes).Relación 92.6% y 7.4%. Problema de subespecificación del modelo : No poder contar con todas las variables y en cambio tener unas pocas que separaban de manera perfecta dicha relacion Permanencia/Abandono (Existencia de limitaciones y supuestos). En cuánto a las acciones comerciales podría existir una posibilidad de mejora, en el churn obtenido, aunque el índice de cancelación de los clientes puede ser asociado a distintos factores, es complicado decidir un porcentaje de churn ideal aunque lo esperado sea tener una rotación muy baja.

In [39]:
print("Probabilidad de Acierto: " +str(y_prob_test_logreg_1[0][y_pred_test_logreg_1]*100)+"%")
Probabilidad de Acierto: [99.79634892 99.79634892 99.79634892 ... 99.79634892 99.79634892
 99.79634892]%
In [35]:
from id_clientes_prob import proba_ID
table_clientes =proba_ID(y_pred_test_logreg_1,y_prob_test_logreg_1,X_test_analysis,"LogisticRegression_1")

Está es la lista de 1565 clientes que el modelo clasificó como los más propensos a no permanecer en telefónica y la probabilidad de abandono que tiene cada cliente , esta lista es la que esperaría obtener la compañía para los respectivos procesos de evaluación una vez generado el modelo ganador. El modelo fue capaz de predecir el 1.69% de los clientes que no permanecerán el mes siguiente (Febrero).

Matriz de Coeficientes

In [49]:
pd.DataFrame(list(zip(X_train_analysis.columns, np.transpose(logreg_1.coef_))))
Out[49]:
0 1
0 facturacion [0.04624543242223228]
1 incidencia [7.574486859117544]
2 mb_datos [0.039707888061456]
3 seg_llamad_ent [0.018166948576596356]
4 seg_llamad_sal [-0.0018932496427915906]
5 financiacion [7.090442353902382]
6 descuentos [10.157864181162356]
7 InfoNumdt [7.552500053316867]

se puede ver como el incremento en segundos consumidos disminuyen la probabilidad de que el cliente se fugue.

Extrayendo un Registro del dataset como ejemplo

In [56]:
print(X_train_analysis.iloc[85])
facturacion      -1.539512
incidencia        0.000000
mb_datos          1.050256
seg_llamad_ent    1.085794
seg_llamad_sal   -0.908783
financiacion      1.000000
descuentos        1.000000
InfoNumdt         1.000000
Name: 87, dtype: float64
In [60]:
F=X_train_analysis.iloc[85]
F.shape
F.values.reshape(1,-1)
logreg_1.predict_proba(F.values.reshape(1,-1))
Out[60]:
array([[0.19948318, 0.80051682]])

Para este cliente la probabilidad de abandono es de un 80%, Descripción : lo que incrementa la probabilidad de que este cliente se fugue son el tener entre 1 a 4 líneas en impago, No tener descuentos, No tener terminales financiados, el incremento en el consumo de llamadas entrantes así como en los mb de datos.

In [38]:
data_= y_pred_test_logreg_1
data_= pd.DataFrame(data_)
data_.columns = ["resul"]
data_["resul"].value_counts()
Out[38]:
0    91146
1     1565
Name: resul, dtype: int64
In [43]:
t = logreg_1.coef_
t = t.transpose()
In [47]:
importance_df = pd.DataFrame(t, columns=['Feature_Importance'],
                             index=X_train_analysis.columns)

importance_df.sort_values(by=['Feature_Importance'], ascending=True).plot(kind="barh",figsize=(12,10))
plt.xticks(rotation=80)
plt.show() 
print(importance_df.sort_values(by=['Feature_Importance'], ascending=False))
                Feature_Importance
descuentos               10.157864
incidencia                7.574487
InfoNumdt                 7.552500
financiacion              7.090442
facturacion               0.046245
mb_datos                  0.039708
seg_llamad_ent            0.018167
seg_llamad_sal           -0.001893

En cuanto a las variables obtenidas posterior al proceso de selección de variables y la estimación del modelo de regresión logística, no es de sorprenderse que la permanencia de los clientes en la compañía dependerá en gran parte de los incentivos y el nivel de satisfacción, por lo que es imposible retener a un cliente insatisfecho , no obstante aún estando satisfechos con el servicio buscan activamente ofertas mejores o "descuentos" y podemos ver que está variable corresponde al top número 1 en el ranking esto es una clara alerta para la compañía una vez teniendo conocimiento de los clientes que están en riesgo de fuga podrá anticiparse a la hipercompetencia y conceder mejores ventajas a sus clientes. Por otro lado la segunda variable más importante Incidencia es claro que los clientes son muy suceptibles a tener una mala percepción del servicio con muy poco esfuerzo, por ejemplo un mal servicio por parte del proveedor , deficiencias en la calidad de los productos y porsupuesto un mal asesoramiento en atención al cliente: como por ejemplo líneas ocupadas el famoso permanezca en espera en breve le atenderemos, largos minutos de espera, errores en la facturación, entre otros.. la tercera variable más importante información sobre número de líneas en impago, no es nuevo que los clíentes de telefonía pueden cambiar libremente de compañía y conservando su número de teléfono aunque mantengan una deuda con la operadora, por lo que los clientes con número de líneas en impago son los más peligrosos ya que son los más inestables y por lo general se acostumbran a cambiar de una compañía a otra para eludir los pagos. Por ùltimo la cuarta variable más importante financiación es claro que los clientes que tengan fiananciado algún tipo de terminal están obligados a permanecer en la compañía contrarío a los que no, quienes tienen toda la libertad de marcharse cuando lo deseen (dependiendo el tipo de contrato y telefónia) podrían deberse a clientes nuevos con expectativas muy altas que sin se incumplen causan malestar y decepción, por ejemplo un grave error al captar clientes es prometer más de lo que se puede dar pero no siendo así como clientes somos un mundo muy incierto bajo constante cambio por lo que hay causas incontrolables en cuanto a la fuga de clientes que no tienen terminales financiados, como cambio de domicilio, fallecimiento del cliente,etc.

In [278]:
#X_train_analysis.to_csv('train.csv', header=True, index=True)
#X_test_analysis.to_csv('test.csv', header=True, index=True)
#target.to_csv('y.csv', header=True, index=True)